diff --git a/.env.example b/.env.example
deleted file mode 100644
index e69de29..0000000
diff --git a/.gitignore b/.gitignore
index 0a0a319..8dd95cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,7 +19,7 @@ wheels/
*.egg-info/
.installed.cfg
*.egg
-
+.history
# Environments
.env
.venv
diff --git a/.history/.dockerignore_20250830102713 b/.history/.dockerignore_20250830102713
deleted file mode 100644
index 33bbec7..0000000
--- a/.history/.dockerignore_20250830102713
+++ /dev/null
@@ -1,58 +0,0 @@
-# Виртуальное окружение
-.venv/
-venv/
-env/
-
-# Кэш Python
-__pycache__/
-*.py[cod]
-*$py.class
-*.so
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# Логи
-logs/
-*.log
-
-# Локальные настройки и данные
-.env.local
-*.db
-*.sqlite3
-
-# Git и GitHub файлы
-.git/
-.github/
-.gitignore
-.gitattributes
-
-# IDE файлы
-.idea/
-.vscode/
-*.swp
-*.swo
-
-# История и временные файлы
-.history/
-*.tmp
-*.bak
-
-# Файлы Windows
-Thumbs.db
-ehthumbs.db
-Desktop.ini
-$RECYCLE.BIN/
diff --git a/.history/.dockerignore_20250830103154 b/.history/.dockerignore_20250830103154
deleted file mode 100644
index 33bbec7..0000000
--- a/.history/.dockerignore_20250830103154
+++ /dev/null
@@ -1,58 +0,0 @@
-# Виртуальное окружение
-.venv/
-venv/
-env/
-
-# Кэш Python
-__pycache__/
-*.py[cod]
-*$py.class
-*.so
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# Логи
-logs/
-*.log
-
-# Локальные настройки и данные
-.env.local
-*.db
-*.sqlite3
-
-# Git и GitHub файлы
-.git/
-.github/
-.gitignore
-.gitattributes
-
-# IDE файлы
-.idea/
-.vscode/
-*.swp
-*.swo
-
-# История и временные файлы
-.history/
-*.tmp
-*.bak
-
-# Файлы Windows
-Thumbs.db
-ehthumbs.db
-Desktop.ini
-$RECYCLE.BIN/
diff --git a/.history/.dockerignore_20250830103255 b/.history/.dockerignore_20250830103255
deleted file mode 100644
index 80703b2..0000000
--- a/.history/.dockerignore_20250830103255
+++ /dev/null
@@ -1,59 +0,0 @@
-# Виртуальное окружение
-.venv/
-venv/
-env/
-.env
-
-# Кэш Python
-__pycache__/
-*.py[cod]
-*$py.class
-*.so
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# Логи
-logs/
-*.log
-
-# Локальные настройки и данные
-.env.local
-*.db
-*.sqlite3
-
-# Git и GitHub файлы
-.git/
-.github/
-.gitignore
-.gitattributes
-
-# IDE файлы
-.idea/
-.vscode/
-*.swp
-*.swo
-
-# История и временные файлы
-.history/
-*.tmp
-*.bak
-
-# Файлы Windows
-Thumbs.db
-ehthumbs.db
-Desktop.ini
-$RECYCLE.BIN/
diff --git a/.history/.env-example_20250830103005 b/.history/.env-example_20250830103005
deleted file mode 100644
index 07edb8a..0000000
--- a/.history/.env-example_20250830103005
+++ /dev/null
@@ -1,26 +0,0 @@
-# Telegram Bot API
-TELEGRAM_TOKEN=your_telegram_bot_token
-ADMIN_USER_IDS=123456789,987654321 # ID пользователей-администраторов через запятую
-
-# Synology NAS
-SYNOLOGY_HOST=192.168.1.100
-SYNOLOGY_PORT=5000 # Обычно 5000 для HTTP и 5001 для HTTPS
-SYNOLOGY_USERNAME=your_username
-SYNOLOGY_PASSWORD=your_password
-SYNOLOGY_SECURE=True # Использовать HTTPS
-SYNOLOGY_VERIFY_SSL=False # Проверка SSL-сертификата
-SYNOLOGY_TIMEOUT=10 # Таймаут для API запросов в секундах
-SYNOLOGY_API_VERSION=1 # Версия API
-SYNOLOGY_POWER_API=SYNO.Core.System # API для управления питанием
-
-# WOL (Wake-on-LAN)
-MAC_ADDRESS=00:11:22:33:44:55 # MAC-адрес Synology NAS
-WOL_BROADCAST=255.255.255.255 # Broadcast-адрес для WOL
-WOL_PORT=9 # Порт для WOL (обычно 7 или 9)
-
-# Logging
-LOG_LEVEL=INFO
-
-# Docker specific
-DOCKER_ENV=true # Указывает, что приложение запущено в Docker
-HEALTHCHECK_PORT=8080 # Порт для healthcheck
diff --git a/.history/.env-example_20250830103154 b/.history/.env-example_20250830103154
deleted file mode 100644
index 07edb8a..0000000
--- a/.history/.env-example_20250830103154
+++ /dev/null
@@ -1,26 +0,0 @@
-# Telegram Bot API
-TELEGRAM_TOKEN=your_telegram_bot_token
-ADMIN_USER_IDS=123456789,987654321 # ID пользователей-администраторов через запятую
-
-# Synology NAS
-SYNOLOGY_HOST=192.168.1.100
-SYNOLOGY_PORT=5000 # Обычно 5000 для HTTP и 5001 для HTTPS
-SYNOLOGY_USERNAME=your_username
-SYNOLOGY_PASSWORD=your_password
-SYNOLOGY_SECURE=True # Использовать HTTPS
-SYNOLOGY_VERIFY_SSL=False # Проверка SSL-сертификата
-SYNOLOGY_TIMEOUT=10 # Таймаут для API запросов в секундах
-SYNOLOGY_API_VERSION=1 # Версия API
-SYNOLOGY_POWER_API=SYNO.Core.System # API для управления питанием
-
-# WOL (Wake-on-LAN)
-MAC_ADDRESS=00:11:22:33:44:55 # MAC-адрес Synology NAS
-WOL_BROADCAST=255.255.255.255 # Broadcast-адрес для WOL
-WOL_PORT=9 # Порт для WOL (обычно 7 или 9)
-
-# Logging
-LOG_LEVEL=INFO
-
-# Docker specific
-DOCKER_ENV=true # Указывает, что приложение запущено в Docker
-HEALTHCHECK_PORT=8080 # Порт для healthcheck
diff --git a/.history/.env_20250830063713 b/.history/.env_20250830063713
deleted file mode 100644
index 890528b..0000000
--- a/.history/.env_20250830063713
+++ /dev/null
@@ -1,15 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=your_telegram_bot_token
-ADMIN_USER_IDS=123456789,987654321
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.1.100
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=admin
-SYNOLOGY_PASSWORD=your_password
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=00:11:22:33:44:55
-WOL_PORT=9
diff --git a/.history/.env_20250830063839 b/.history/.env_20250830063839
deleted file mode 100644
index 890528b..0000000
--- a/.history/.env_20250830063839
+++ /dev/null
@@ -1,15 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=your_telegram_bot_token
-ADMIN_USER_IDS=123456789,987654321
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.1.100
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=admin
-SYNOLOGY_PASSWORD=your_password
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=00:11:22:33:44:55
-WOL_PORT=9
diff --git a/.history/.env_20250830071119 b/.history/.env_20250830071119
deleted file mode 100644
index 7c53ffa..0000000
--- a/.history/.env_20250830071119
+++ /dev/null
@@ -1,15 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=your_telegram_bot_token
-ADMIN_USER_IDS=123456789,987654321
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.0.
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=admin
-SYNOLOGY_PASSWORD=your_password
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=00:11:22:33:44:55
-WOL_PORT=9
diff --git a/.history/.env_20250830071139 b/.history/.env_20250830071139
deleted file mode 100644
index 66be270..0000000
--- a/.history/.env_20250830071139
+++ /dev/null
@@ -1,15 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=your_telegram_bot_token
-ADMIN_USER_IDS=123456789,987654321
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.0.102
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=superadmin
-SYNOLOGY_PASSWORD=Cl0ud_1985!
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=00:11:22:33:44:55
-WOL_PORT=9
diff --git a/.history/.env_20250830071153 b/.history/.env_20250830071153
deleted file mode 100644
index c784fd2..0000000
--- a/.history/.env_20250830071153
+++ /dev/null
@@ -1,15 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=your_telegram_bot_token
-ADMIN_USER_IDS=123456789,987654321
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.0.102
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=superadmin
-SYNOLOGY_PASSWORD=Cl0ud_1985!
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=90:09:D0:8C:27:F9
-WOL_PORT=9
diff --git a/.history/.env_20250830071249 b/.history/.env_20250830071249
deleted file mode 100644
index a12eb00..0000000
--- a/.history/.env_20250830071249
+++ /dev/null
@@ -1,15 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY
-ADMIN_USER_IDS=123456789,987654321
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.0.102
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=superadmin
-SYNOLOGY_PASSWORD=Cl0ud_1985!
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=90:09:D0:8C:27:F9
-WOL_PORT=9
diff --git a/.history/.env_20250830071300 b/.history/.env_20250830071300
deleted file mode 100644
index 898235d..0000000
--- a/.history/.env_20250830071300
+++ /dev/null
@@ -1,15 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY
-ADMIN_USER_IDS=556399210
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.0.102
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=superadmin
-SYNOLOGY_PASSWORD=Cl0ud_1985!
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=90:09:D0:8C:27:F9
-WOL_PORT=9
diff --git a/.history/.env_20250830072439.example b/.history/.env_20250830072439.example
deleted file mode 100644
index bc8cf69..0000000
--- a/.history/.env_20250830072439.example
+++ /dev/null
@@ -1,16 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=your_telegram_bot_token
-ADMIN_USER_IDS=123456789,987654321
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.1.100
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=admin
-SYNOLOGY_PASSWORD=your_password
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-SYNOLOGY_API_VERSION=1
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=00:11:22:33:44:55
-WOL_PORT=9
diff --git a/.history/.env_20250830072817.example b/.history/.env_20250830072817.example
deleted file mode 100644
index bc8cf69..0000000
--- a/.history/.env_20250830072817.example
+++ /dev/null
@@ -1,16 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=your_telegram_bot_token
-ADMIN_USER_IDS=123456789,987654321
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.1.100
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=admin
-SYNOLOGY_PASSWORD=your_password
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-SYNOLOGY_API_VERSION=1
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=00:11:22:33:44:55
-WOL_PORT=9
diff --git a/.history/.env_20250830080745 b/.history/.env_20250830080745
deleted file mode 100644
index aab416d..0000000
--- a/.history/.env_20250830080745
+++ /dev/null
@@ -1,16 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY
-ADMIN_USER_IDS=556399210
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.0.102
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=superadmin
-SYNOLOGY_PASSWORD=Cl0ud_1985!
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-SYNOLOGY_API_VERSION=1
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=90:09:D0:8C:27:F9
-WOL_PORT=9
diff --git a/.history/.env_20250830082200 b/.history/.env_20250830082200
deleted file mode 100644
index 828e005..0000000
--- a/.history/.env_20250830082200
+++ /dev/null
@@ -1,16 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY
-ADMIN_USER_IDS=556399210
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.0.102
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=superadmin
-SYNOLOGY_PASSWORD=Cl0ud_1985!
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-SYNOLOGY_API_VERSION=2
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=90:09:D0:8C:27:F9
-WOL_PORT=9
diff --git a/.history/.env_20250830082210 b/.history/.env_20250830082210
deleted file mode 100644
index af5bd96..0000000
--- a/.history/.env_20250830082210
+++ /dev/null
@@ -1,20 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY
-ADMIN_USER_IDS=556399210
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.0.102
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=superadmin
-SYNOLOGY_PASSWORD=Cl0ud_1985!
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-SYNOLOGY_API_VERSION=2
-
-# API Configuration
-SYNOLOGY_POWER_API=SYNO.Core.Hardware.PowerRecovery
-SYNOLOGY_INFO_API=SYNO.DSM.Info
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=90:09:D0:8C:27:F9
-WOL_PORT=9
diff --git a/.history/.env_20250830082500 b/.history/.env_20250830082500
deleted file mode 100644
index af5bd96..0000000
--- a/.history/.env_20250830082500
+++ /dev/null
@@ -1,20 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY
-ADMIN_USER_IDS=556399210
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.0.102
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=superadmin
-SYNOLOGY_PASSWORD=Cl0ud_1985!
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-SYNOLOGY_API_VERSION=2
-
-# API Configuration
-SYNOLOGY_POWER_API=SYNO.Core.Hardware.PowerRecovery
-SYNOLOGY_INFO_API=SYNO.DSM.Info
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=90:09:D0:8C:27:F9
-WOL_PORT=9
diff --git a/.history/.env_20250830083237 b/.history/.env_20250830083237
deleted file mode 100644
index b67e336..0000000
--- a/.history/.env_20250830083237
+++ /dev/null
@@ -1,20 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY
-ADMIN_USER_IDS=556399210
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.0.102
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=superadmin
-SYNOLOGY_PASSWORD=Cl0ud_1985!
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-SYNOLOGY_API_VERSION=6
-
-# API Configuration
-SYNOLOGY_POWER_API=SYNO.Core.Hardware.PowerRecovery
-SYNOLOGY_INFO_API=SYNO.DSM.Info
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=90:09:D0:8C:27:F9
-WOL_PORT=9
diff --git a/.history/.env_20250830101005 b/.history/.env_20250830101005
deleted file mode 100644
index 82e6f2a..0000000
--- a/.history/.env_20250830101005
+++ /dev/null
@@ -1,20 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY
-ADMIN_USER_IDS=556399210
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.0.102
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=superadmin
-SYNOLOGY_PASSWORD=Cl0ud_1985!
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-SYNOLOGY_API_VERSION=6
-
-# API Configuration
-SYNOLOGY_POWER_API=SYNO.Core.System
-SYNOLOGY_INFO_API=SYNO.DSM.Info
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=90:09:D0:8C:27:F9
-WOL_PORT=9
diff --git a/.history/.env_20250830101843 b/.history/.env_20250830101843
deleted file mode 100644
index 82e6f2a..0000000
--- a/.history/.env_20250830101843
+++ /dev/null
@@ -1,20 +0,0 @@
-# Telegram Bot Configuration
-TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY
-ADMIN_USER_IDS=556399210
-
-# Synology NAS Configuration
-SYNOLOGY_HOST=192.168.0.102
-SYNOLOGY_PORT=5000
-SYNOLOGY_USERNAME=superadmin
-SYNOLOGY_PASSWORD=Cl0ud_1985!
-SYNOLOGY_SECURE=False
-SYNOLOGY_TIMEOUT=10
-SYNOLOGY_API_VERSION=6
-
-# API Configuration
-SYNOLOGY_POWER_API=SYNO.Core.System
-SYNOLOGY_INFO_API=SYNO.DSM.Info
-
-# Wake-on-LAN Configuration
-SYNOLOGY_MAC=90:09:D0:8C:27:F9
-WOL_PORT=9
diff --git a/.history/.gitignore_20250830063748 b/.history/.gitignore_20250830063748
deleted file mode 100644
index 0a0a319..0000000
--- a/.history/.gitignore_20250830063748
+++ /dev/null
@@ -1,44 +0,0 @@
-# Python
-__pycache__/
-*.py[cod]
-*$py.class
-*.so
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Logs
-logs/
-*.log
-
-# IDE specific files
-.idea/
-.vscode/
-*.swp
-*.swo
-
-# OS specific files
-.DS_Store
-Thumbs.db
diff --git a/.history/.gitignore_20250830063839 b/.history/.gitignore_20250830063839
deleted file mode 100644
index 0a0a319..0000000
--- a/.history/.gitignore_20250830063839
+++ /dev/null
@@ -1,44 +0,0 @@
-# Python
-__pycache__/
-*.py[cod]
-*$py.class
-*.so
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Logs
-logs/
-*.log
-
-# IDE specific files
-.idea/
-.vscode/
-*.swp
-*.swo
-
-# OS specific files
-.DS_Store
-Thumbs.db
diff --git a/.history/DOCKER_DEPLOYMENT_20250830103243.md b/.history/DOCKER_DEPLOYMENT_20250830103243.md
deleted file mode 100644
index 4e5b666..0000000
--- a/.history/DOCKER_DEPLOYMENT_20250830103243.md
+++ /dev/null
@@ -1,218 +0,0 @@
-# Synology Power Control Bot - Руководство по развертыванию в Docker
-
-## Подготовка к развертыванию
-
-Это руководство поможет вам развернуть бота для управления питанием Synology NAS в Docker-контейнере. Развертывание в Docker имеет следующие преимущества:
-- Изоляция приложения и его зависимостей
-- Простота управления и обновления
-- Автоматический перезапуск при сбоях
-- Возможность легкого переноса между системами
-
-## Предварительные требования
-
-1. **Установка Docker и Docker Compose**:
-
- **Для Ubuntu/Debian**:
- ```bash
- # Установка Docker
- sudo apt update
- sudo apt install apt-transport-https ca-certificates curl software-properties-common
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
- sudo apt update
- sudo apt install docker-ce docker-ce-cli containerd.io
-
- # Установка Docker Compose
- sudo curl -L "https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
- sudo chmod +x /usr/local/bin/docker-compose
- ```
-
- **Для CentOS/RHEL**:
- ```bash
- # Установка Docker
- sudo yum install -y yum-utils
- sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
- sudo yum install docker-ce docker-ce-cli containerd.io
- sudo systemctl start docker
- sudo systemctl enable docker
-
- # Установка Docker Compose
- sudo curl -L "https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
- sudo chmod +x /usr/local/bin/docker-compose
- ```
-
- **Для Windows**:
- - Скачайте и установите Docker Desktop с [официального сайта Docker](https://www.docker.com/products/docker-desktop/)
-
-2. **Настройка проекта**:
- ```bash
- # Клонирование репозитория (если используете Git)
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
-
- # Или распакуйте архив с исходным кодом
- ```
-
-## Конфигурация
-
-1. **Создайте файл .env**:
- Создайте файл `.env` на основе `.env-example` и настройте его с вашими параметрами:
-
- ```bash
- cp .env-example .env
- nano .env # или любой другой текстовый редактор
- ```
-
- Заполните следующие параметры:
- - `TELEGRAM_TOKEN`: Токен вашего Telegram-бота от @BotFather
- - `ADMIN_USER_IDS`: ID пользователей Telegram с доступом к боту
- - `SYNOLOGY_HOST`: IP-адрес вашего Synology NAS
- - `SYNOLOGY_USERNAME` и `SYNOLOGY_PASSWORD`: Учетные данные для DSM
- - `MAC_ADDRESS`: MAC-адрес Synology NAS для Wake-on-LAN
-
-## Развертывание
-
-### С использованием скриптов:
-
-**Linux**:
-```bash
-chmod +x deploy.sh
-./deploy.sh
-```
-
-**Windows**:
-```
-deploy.cmd
-```
-
-### Вручную с Docker Compose:
-
-1. **Сборка и запуск**:
- ```bash
- docker-compose up -d --build
- ```
-
-2. **Проверка статуса**:
- ```bash
- docker-compose ps
- ```
-
-3. **Просмотр логов**:
- ```bash
- docker-compose logs -f
- ```
-
-4. **Остановка**:
- ```bash
- docker-compose down
- ```
-
-## Управление контейнером
-
-### Перезапуск бота:
-```bash
-docker-compose restart
-```
-
-### Обновление:
-```bash
-# Остановка
-docker-compose down
-
-# Обновление (если используете Git)
-git pull
-
-# Пересборка и запуск
-docker-compose up -d --build
-```
-
-### Резервное копирование данных:
-Важные данные хранятся в томе `logs`, который можно скопировать:
-```bash
-# Создание бэкапа логов
-tar -czvf synology_bot_logs_backup.tar.gz ./logs
-```
-
-## Проверка работоспособности
-
-После развертывания можно проверить состояние бота с помощью следующих команд:
-
-1. **Проверка статуса контейнера**:
- ```bash
- docker-compose ps
- ```
-
-2. **Проверка health-check**:
- ```bash
- curl http://localhost:8080/health
- ```
- Должен вернуть `OK`.
-
-3. **Проверка логов**:
- ```bash
- docker-compose logs -f
- ```
- Ищите строки с успешной инициализацией бота.
-
-## Решение проблем
-
-### Контейнер не запускается или сразу завершает работу
-- Проверьте логи: `docker-compose logs -f`
-- Проверьте файл `.env` на наличие всех необходимых параметров
-- Убедитесь, что порт 8080 не занят другим приложением
-
-### Проблемы с подключением к Synology NAS
-- Проверьте доступность NAS из контейнера:
- ```bash
- docker-compose exec synology-bot ping $SYNOLOGY_HOST
- ```
-- Проверьте правильность учетных данных
-- Убедитесь, что API DSM включено в настройках NAS
-
-### Telegram-бот не отвечает
-- Проверьте корректность TELEGRAM_TOKEN
-- Убедитесь, что бот запущен: `/start` в чате с ботом
-- Проверьте, что ваш Telegram ID указан в ADMIN_USER_IDS
-
-## Автоматический запуск при перезагрузке сервера
-
-Docker и Docker Compose по умолчанию настроены на автоматический запуск контейнеров при перезагрузке системы благодаря параметру `restart: unless-stopped` в docker-compose.yml.
-
-Если эта опция не работает, вы можете настроить systemd:
-
-1. **Создайте файл сервиса**:
- ```bash
- sudo nano /etc/systemd/system/synology-bot.service
- ```
-
-2. **Добавьте следующее содержимое**:
- ```
- [Unit]
- Description=Synology Power Control Bot
- Requires=docker.service
- After=docker.service
-
- [Service]
- Type=oneshot
- RemainAfterExit=yes
- WorkingDirectory=/path/to/synology_power_control_bot
- ExecStart=/usr/local/bin/docker-compose up -d
- ExecStop=/usr/local/bin/docker-compose down
- TimeoutStartSec=0
-
- [Install]
- WantedBy=multi-user.target
- ```
-
-3. **Активируйте и запустите сервис**:
- ```bash
- sudo systemctl enable synology-bot.service
- sudo systemctl start synology-bot.service
- ```
-
-## Безопасность
-
-- Не передавайте файл `.env` с учетными данными третьим лицам
-- Регулярно меняйте пароль от DSM
-- Ограничьте доступ к боту только доверенным пользователям
-- Рассмотрите возможность запуска на выделенной сети или с дополнительными ограничениями доступа
diff --git a/.history/DOCKER_DEPLOYMENT_20250830103340.md b/.history/DOCKER_DEPLOYMENT_20250830103340.md
deleted file mode 100644
index 4e5b666..0000000
--- a/.history/DOCKER_DEPLOYMENT_20250830103340.md
+++ /dev/null
@@ -1,218 +0,0 @@
-# Synology Power Control Bot - Руководство по развертыванию в Docker
-
-## Подготовка к развертыванию
-
-Это руководство поможет вам развернуть бота для управления питанием Synology NAS в Docker-контейнере. Развертывание в Docker имеет следующие преимущества:
-- Изоляция приложения и его зависимостей
-- Простота управления и обновления
-- Автоматический перезапуск при сбоях
-- Возможность легкого переноса между системами
-
-## Предварительные требования
-
-1. **Установка Docker и Docker Compose**:
-
- **Для Ubuntu/Debian**:
- ```bash
- # Установка Docker
- sudo apt update
- sudo apt install apt-transport-https ca-certificates curl software-properties-common
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
- sudo apt update
- sudo apt install docker-ce docker-ce-cli containerd.io
-
- # Установка Docker Compose
- sudo curl -L "https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
- sudo chmod +x /usr/local/bin/docker-compose
- ```
-
- **Для CentOS/RHEL**:
- ```bash
- # Установка Docker
- sudo yum install -y yum-utils
- sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
- sudo yum install docker-ce docker-ce-cli containerd.io
- sudo systemctl start docker
- sudo systemctl enable docker
-
- # Установка Docker Compose
- sudo curl -L "https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
- sudo chmod +x /usr/local/bin/docker-compose
- ```
-
- **Для Windows**:
- - Скачайте и установите Docker Desktop с [официального сайта Docker](https://www.docker.com/products/docker-desktop/)
-
-2. **Настройка проекта**:
- ```bash
- # Клонирование репозитория (если используете Git)
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
-
- # Или распакуйте архив с исходным кодом
- ```
-
-## Конфигурация
-
-1. **Создайте файл .env**:
- Создайте файл `.env` на основе `.env-example` и настройте его с вашими параметрами:
-
- ```bash
- cp .env-example .env
- nano .env # или любой другой текстовый редактор
- ```
-
- Заполните следующие параметры:
- - `TELEGRAM_TOKEN`: Токен вашего Telegram-бота от @BotFather
- - `ADMIN_USER_IDS`: ID пользователей Telegram с доступом к боту
- - `SYNOLOGY_HOST`: IP-адрес вашего Synology NAS
- - `SYNOLOGY_USERNAME` и `SYNOLOGY_PASSWORD`: Учетные данные для DSM
- - `MAC_ADDRESS`: MAC-адрес Synology NAS для Wake-on-LAN
-
-## Развертывание
-
-### С использованием скриптов:
-
-**Linux**:
-```bash
-chmod +x deploy.sh
-./deploy.sh
-```
-
-**Windows**:
-```
-deploy.cmd
-```
-
-### Вручную с Docker Compose:
-
-1. **Сборка и запуск**:
- ```bash
- docker-compose up -d --build
- ```
-
-2. **Проверка статуса**:
- ```bash
- docker-compose ps
- ```
-
-3. **Просмотр логов**:
- ```bash
- docker-compose logs -f
- ```
-
-4. **Остановка**:
- ```bash
- docker-compose down
- ```
-
-## Управление контейнером
-
-### Перезапуск бота:
-```bash
-docker-compose restart
-```
-
-### Обновление:
-```bash
-# Остановка
-docker-compose down
-
-# Обновление (если используете Git)
-git pull
-
-# Пересборка и запуск
-docker-compose up -d --build
-```
-
-### Резервное копирование данных:
-Важные данные хранятся в томе `logs`, который можно скопировать:
-```bash
-# Создание бэкапа логов
-tar -czvf synology_bot_logs_backup.tar.gz ./logs
-```
-
-## Проверка работоспособности
-
-После развертывания можно проверить состояние бота с помощью следующих команд:
-
-1. **Проверка статуса контейнера**:
- ```bash
- docker-compose ps
- ```
-
-2. **Проверка health-check**:
- ```bash
- curl http://localhost:8080/health
- ```
- Должен вернуть `OK`.
-
-3. **Проверка логов**:
- ```bash
- docker-compose logs -f
- ```
- Ищите строки с успешной инициализацией бота.
-
-## Решение проблем
-
-### Контейнер не запускается или сразу завершает работу
-- Проверьте логи: `docker-compose logs -f`
-- Проверьте файл `.env` на наличие всех необходимых параметров
-- Убедитесь, что порт 8080 не занят другим приложением
-
-### Проблемы с подключением к Synology NAS
-- Проверьте доступность NAS из контейнера:
- ```bash
- docker-compose exec synology-bot ping $SYNOLOGY_HOST
- ```
-- Проверьте правильность учетных данных
-- Убедитесь, что API DSM включено в настройках NAS
-
-### Telegram-бот не отвечает
-- Проверьте корректность TELEGRAM_TOKEN
-- Убедитесь, что бот запущен: `/start` в чате с ботом
-- Проверьте, что ваш Telegram ID указан в ADMIN_USER_IDS
-
-## Автоматический запуск при перезагрузке сервера
-
-Docker и Docker Compose по умолчанию настроены на автоматический запуск контейнеров при перезагрузке системы благодаря параметру `restart: unless-stopped` в docker-compose.yml.
-
-Если эта опция не работает, вы можете настроить systemd:
-
-1. **Создайте файл сервиса**:
- ```bash
- sudo nano /etc/systemd/system/synology-bot.service
- ```
-
-2. **Добавьте следующее содержимое**:
- ```
- [Unit]
- Description=Synology Power Control Bot
- Requires=docker.service
- After=docker.service
-
- [Service]
- Type=oneshot
- RemainAfterExit=yes
- WorkingDirectory=/path/to/synology_power_control_bot
- ExecStart=/usr/local/bin/docker-compose up -d
- ExecStop=/usr/local/bin/docker-compose down
- TimeoutStartSec=0
-
- [Install]
- WantedBy=multi-user.target
- ```
-
-3. **Активируйте и запустите сервис**:
- ```bash
- sudo systemctl enable synology-bot.service
- sudo systemctl start synology-bot.service
- ```
-
-## Безопасность
-
-- Не передавайте файл `.env` с учетными данными третьим лицам
-- Регулярно меняйте пароль от DSM
-- Ограничьте доступ к боту только доверенным пользователям
-- Рассмотрите возможность запуска на выделенной сети или с дополнительными ограничениями доступа
diff --git a/.history/Dockerfile_20250830102631 b/.history/Dockerfile_20250830102631
deleted file mode 100644
index c29324d..0000000
--- a/.history/Dockerfile_20250830102631
+++ /dev/null
@@ -1,20 +0,0 @@
-FROM python:3.11-slim
-
-# Устанавливаем рабочую директорию
-WORKDIR /app
-
-# Копируем файлы зависимостей
-COPY requirements.txt .
-
-# Устанавливаем зависимости
-RUN pip install --no-cache-dir -r requirements.txt
-
-# Копируем исходный код
-COPY . .
-
-# Указываем переменные окружения
-ENV PYTHONPATH=/app
-ENV PYTHONUNBUFFERED=1
-
-# Запускаем приложение
-CMD ["python", "run.py"]
diff --git a/.history/Dockerfile_20250830102802 b/.history/Dockerfile_20250830102802
deleted file mode 100644
index e08df4b..0000000
--- a/.history/Dockerfile_20250830102802
+++ /dev/null
@@ -1,23 +0,0 @@
-FROM python:3.11-slim
-
-# Устанавливаем рабочую директорию
-WORKDIR /app
-
-# Копируем файлы зависимостей
-COPY requirements.txt .
-
-# Устанавливаем зависимости
-RUN pip install --no-cache-dir -r requirements.txt
-
-# Копируем исходный код
-COPY . .
-
-# Делаем entrypoint исполняемым
-RUN chmod +x /app/entrypoint.sh
-
-# Указываем переменные окружения
-ENV PYTHONPATH=/app
-ENV PYTHONUNBUFFERED=1
-
-# Используем entrypoint.sh
-ENTRYPOINT ["/app/entrypoint.sh"]
diff --git a/.history/Dockerfile_20250830103154 b/.history/Dockerfile_20250830103154
deleted file mode 100644
index e08df4b..0000000
--- a/.history/Dockerfile_20250830103154
+++ /dev/null
@@ -1,23 +0,0 @@
-FROM python:3.11-slim
-
-# Устанавливаем рабочую директорию
-WORKDIR /app
-
-# Копируем файлы зависимостей
-COPY requirements.txt .
-
-# Устанавливаем зависимости
-RUN pip install --no-cache-dir -r requirements.txt
-
-# Копируем исходный код
-COPY . .
-
-# Делаем entrypoint исполняемым
-RUN chmod +x /app/entrypoint.sh
-
-# Указываем переменные окружения
-ENV PYTHONPATH=/app
-ENV PYTHONUNBUFFERED=1
-
-# Используем entrypoint.sh
-ENTRYPOINT ["/app/entrypoint.sh"]
diff --git a/.history/README_20250830063733.md b/.history/README_20250830063733.md
deleted file mode 100644
index 4145dc8..0000000
--- a/.history/README_20250830063733.md
+++ /dev/null
@@ -1,101 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы
-- ✅ Проверка статуса и получение информации о системе
-- ✅ Ограничение доступа по ID пользователей
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-
-## Установка и настройка
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python -m src.bot
- ```
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS
-- `/help` - Вывод справочной информации
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .gitignore # Файл игнорирования Git
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830063839.md b/.history/README_20250830063839.md
deleted file mode 100644
index 4145dc8..0000000
--- a/.history/README_20250830063839.md
+++ /dev/null
@@ -1,101 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы
-- ✅ Проверка статуса и получение информации о системе
-- ✅ Ограничение доступа по ID пользователей
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-
-## Установка и настройка
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python -m src.bot
- ```
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS
-- `/help` - Вывод справочной информации
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .gitignore # Файл игнорирования Git
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830065402.md b/.history/README_20250830065402.md
deleted file mode 100644
index 6733e92..0000000
--- a/.history/README_20250830065402.md
+++ /dev/null
@@ -1,117 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-
-## Установка и настройка
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python -m src.bot
- ```
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS
-- `/help` - Вывод справочной информации
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .gitignore # Файл игнорирования Git
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830065412.md b/.history/README_20250830065412.md
deleted file mode 100644
index 669dbaa..0000000
--- a/.history/README_20250830065412.md
+++ /dev/null
@@ -1,125 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-
-## Установка и настройка
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python -m src.bot
- ```
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-### Основные команды
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS (включение, выключение, перезагрузка)
-- `/help` - Вывод справочной информации
-
-### Расширенные команды
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .gitignore # Файл игнорирования Git
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830065454.md b/.history/README_20250830065454.md
deleted file mode 100644
index 669dbaa..0000000
--- a/.history/README_20250830065454.md
+++ /dev/null
@@ -1,125 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-
-## Установка и настройка
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python -m src.bot
- ```
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-### Основные команды
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS (включение, выключение, перезагрузка)
-- `/help` - Вывод справочной информации
-
-### Расширенные команды
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .gitignore # Файл игнорирования Git
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830072408.md b/.history/README_20250830072408.md
deleted file mode 100644
index c7b18ca..0000000
--- a/.history/README_20250830072408.md
+++ /dev/null
@@ -1,126 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-- Доступ к порту API Synology DSM (обычно 5000 или 5001)
-
-## Установка и настройка
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python -m src.bot
- ```
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-### Основные команды
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS (включение, выключение, перезагрузка)
-- `/help` - Вывод справочной информации
-
-### Расширенные команды
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .gitignore # Файл игнорирования Git
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830072817.md b/.history/README_20250830072817.md
deleted file mode 100644
index c7b18ca..0000000
--- a/.history/README_20250830072817.md
+++ /dev/null
@@ -1,126 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-- Доступ к порту API Synology DSM (обычно 5000 или 5001)
-
-## Установка и настройка
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python -m src.bot
- ```
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-### Основные команды
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS (включение, выключение, перезагрузка)
-- `/help` - Вывод справочной информации
-
-### Расширенные команды
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .gitignore # Файл игнорирования Git
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830092253.md b/.history/README_20250830092253.md
deleted file mode 100644
index 6d82398..0000000
--- a/.history/README_20250830092253.md
+++ /dev/null
@@ -1,128 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-- ✅ Список активных процессов
-- ✅ Мониторинг сетевых подключений
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-- Доступ к порту API Synology DSM (обычно 5000 или 5001)
-
-## Установка и настройка
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python -m src.bot
- ```
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-### Основные команды
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS (включение, выключение, перезагрузка)
-- `/help` - Вывод справочной информации
-
-### Расширенные команды
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .gitignore # Файл игнорирования Git
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830092310.md b/.history/README_20250830092310.md
deleted file mode 100644
index d68a352..0000000
--- a/.history/README_20250830092310.md
+++ /dev/null
@@ -1,131 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-- ✅ Список активных процессов
-- ✅ Мониторинг сетевых подключений
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-- ✅ Просмотр файлов и папок
-- ✅ Поиск файлов
-- ✅ Мониторинг квот пользователей
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-- Доступ к порту API Synology DSM (обычно 5000 или 5001)
-
-## Установка и настройка
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python -m src.bot
- ```
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-### Основные команды
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS (включение, выключение, перезагрузка)
-- `/help` - Вывод справочной информации
-
-### Расширенные команды
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .gitignore # Файл игнорирования Git
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830092330.md b/.history/README_20250830092330.md
deleted file mode 100644
index 487bc81..0000000
--- a/.history/README_20250830092330.md
+++ /dev/null
@@ -1,146 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-- ✅ Список активных процессов
-- ✅ Мониторинг сетевых подключений
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-- ✅ Просмотр файлов и папок
-- ✅ Поиск файлов
-- ✅ Мониторинг квот пользователей
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-- Доступ к порту API Synology DSM (обычно 5000 или 5001)
-
-## Установка и настройка
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python -m src.bot
- ```
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-### Основные команды
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS (включение, выключение, перезагрузка)
-- `/help` - Вывод справочной информации
-
-### Информационные команды
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-- `/temperature` - Температура устройства
-- `/processes` - Список активных процессов
-- `/network` - Сетевая информация
-
-### Расширенные команды
-- `/schedule` - Расписание питания
-- `/browse` - Просмотр файлов
-- `/search <запрос>` - Поиск файлов
-- `/updates` - Проверка обновлений
-- `/backup` - Статус резервного копирования
-- `/quota` - Квоты пользователей
-
-### Быстрые команды
-- `/quickreboot` - Быстрая перезагрузка
-- `/wakeup` - Пробуждение NAS (WOL)
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .gitignore # Файл игнорирования Git
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830092350.md b/.history/README_20250830092350.md
deleted file mode 100644
index 5e2e7d0..0000000
--- a/.history/README_20250830092350.md
+++ /dev/null
@@ -1,151 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-- ✅ Список активных процессов
-- ✅ Мониторинг сетевых подключений
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-- ✅ Просмотр файлов и папок
-- ✅ Поиск файлов
-- ✅ Мониторинг квот пользователей
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-### Дополнительные функции
-- ✅ Мониторинг обновлений DSM и пакетов
-- ✅ Управление расписанием питания
-- ✅ Проверка статуса резервного копирования
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-- Доступ к порту API Synology DSM (обычно 5000 или 5001)
-
-## Установка и настройка
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python -m src.bot
- ```
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-### Основные команды
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS (включение, выключение, перезагрузка)
-- `/help` - Вывод справочной информации
-
-### Информационные команды
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-- `/temperature` - Температура устройства
-- `/processes` - Список активных процессов
-- `/network` - Сетевая информация
-
-### Расширенные команды
-- `/schedule` - Расписание питания
-- `/browse` - Просмотр файлов
-- `/search <запрос>` - Поиск файлов
-- `/updates` - Проверка обновлений
-- `/backup` - Статус резервного копирования
-- `/quota` - Квоты пользователей
-
-### Быстрые команды
-- `/quickreboot` - Быстрая перезагрузка
-- `/wakeup` - Пробуждение NAS (WOL)
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .gitignore # Файл игнорирования Git
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830092440.md b/.history/README_20250830092440.md
deleted file mode 100644
index 5e2e7d0..0000000
--- a/.history/README_20250830092440.md
+++ /dev/null
@@ -1,151 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-- ✅ Список активных процессов
-- ✅ Мониторинг сетевых подключений
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-- ✅ Просмотр файлов и папок
-- ✅ Поиск файлов
-- ✅ Мониторинг квот пользователей
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-### Дополнительные функции
-- ✅ Мониторинг обновлений DSM и пакетов
-- ✅ Управление расписанием питания
-- ✅ Проверка статуса резервного копирования
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-- Доступ к порту API Synology DSM (обычно 5000 или 5001)
-
-## Установка и настройка
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python -m src.bot
- ```
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-### Основные команды
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS (включение, выключение, перезагрузка)
-- `/help` - Вывод справочной информации
-
-### Информационные команды
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-- `/temperature` - Температура устройства
-- `/processes` - Список активных процессов
-- `/network` - Сетевая информация
-
-### Расширенные команды
-- `/schedule` - Расписание питания
-- `/browse` - Просмотр файлов
-- `/search <запрос>` - Поиск файлов
-- `/updates` - Проверка обновлений
-- `/backup` - Статус резервного копирования
-- `/quota` - Квоты пользователей
-
-### Быстрые команды
-- `/quickreboot` - Быстрая перезагрузка
-- `/wakeup` - Пробуждение NAS (WOL)
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .gitignore # Файл игнорирования Git
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830103059.md b/.history/README_20250830103059.md
deleted file mode 100644
index bfc3201..0000000
--- a/.history/README_20250830103059.md
+++ /dev/null
@@ -1,192 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-- ✅ Список активных процессов
-- ✅ Мониторинг сетевых подключений
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-- ✅ Просмотр файлов и папок
-- ✅ Поиск файлов
-- ✅ Мониторинг квот пользователей
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-### Дополнительные функции
-- ✅ Мониторинг обновлений DSM и пакетов
-- ✅ Управление расписанием питания
-- ✅ Проверка статуса резервного копирования
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-- Доступ к порту API Synology DSM (обычно 5000 или 5001)
-
-## Установка и настройка
-
-### Метод 1: Локальный запуск
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python run.py
- ```
-
-### Метод 2: Docker
-
-1. Убедитесь, что Docker и Docker Compose установлены в вашей системе.
-
-2. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-3. Создайте файл `.env` на основе `.env-example` и заполните необходимыми значениями.
-
-4. Запустите скрипт развертывания:
- ```bash
- # Linux/macOS
- chmod +x deploy.sh
- ./deploy.sh
-
- # Windows
- deploy.cmd
- ```
-
- Или запустите вручную:
- ```bash
- docker-compose up -d --build
- ```
-
-5. Проверьте статус:
- ```bash
- docker-compose ps
- ```
-
-6. Просмотр логов:
- ```bash
- docker-compose logs -f
- ```
-
-Дополнительная информация о Docker-развертывании доступна в файле [README_DOCKER.md](README_DOCKER.md).
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-### Основные команды
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS (включение, выключение, перезагрузка)
-- `/help` - Вывод справочной информации
-
-### Информационные команды
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-- `/temperature` - Температура устройства
-- `/processes` - Список активных процессов
-- `/network` - Сетевая информация
-
-### Расширенные команды
-- `/schedule` - Расписание питания
-- `/browse` - Просмотр файлов
-- `/search <запрос>` - Поиск файлов
-- `/updates` - Проверка обновлений
-- `/backup` - Статус резервного копирования
-- `/quota` - Квоты пользователей
-
-### Быстрые команды
-- `/quickreboot` - Быстрая перезагрузка
-- `/wakeup` - Пробуждение NAS (WOL)
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .gitignore # Файл игнорирования Git
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830103119.md b/.history/README_20250830103119.md
deleted file mode 100644
index 5e890a6..0000000
--- a/.history/README_20250830103119.md
+++ /dev/null
@@ -1,201 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-- ✅ Список активных процессов
-- ✅ Мониторинг сетевых подключений
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-- ✅ Просмотр файлов и папок
-- ✅ Поиск файлов
-- ✅ Мониторинг квот пользователей
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-### Дополнительные функции
-- ✅ Мониторинг обновлений DSM и пакетов
-- ✅ Управление расписанием питания
-- ✅ Проверка статуса резервного копирования
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-- Доступ к порту API Synology DSM (обычно 5000 или 5001)
-
-## Установка и настройка
-
-### Метод 1: Локальный запуск
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python run.py
- ```
-
-### Метод 2: Docker
-
-1. Убедитесь, что Docker и Docker Compose установлены в вашей системе.
-
-2. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-3. Создайте файл `.env` на основе `.env-example` и заполните необходимыми значениями.
-
-4. Запустите скрипт развертывания:
- ```bash
- # Linux/macOS
- chmod +x deploy.sh
- ./deploy.sh
-
- # Windows
- deploy.cmd
- ```
-
- Или запустите вручную:
- ```bash
- docker-compose up -d --build
- ```
-
-5. Проверьте статус:
- ```bash
- docker-compose ps
- ```
-
-6. Просмотр логов:
- ```bash
- docker-compose logs -f
- ```
-
-Дополнительная информация о Docker-развертывании доступна в файле [README_DOCKER.md](README_DOCKER.md).
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-### Основные команды
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS (включение, выключение, перезагрузка)
-- `/help` - Вывод справочной информации
-
-### Информационные команды
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-- `/temperature` - Температура устройства
-- `/processes` - Список активных процессов
-- `/network` - Сетевая информация
-
-### Расширенные команды
-- `/schedule` - Расписание питания
-- `/browse` - Просмотр файлов
-- `/search <запрос>` - Поиск файлов
-- `/updates` - Проверка обновлений
-- `/backup` - Статус резервного копирования
-- `/quota` - Квоты пользователей
-
-### Быстрые команды
-- `/quickreboot` - Быстрая перезагрузка
-- `/wakeup` - Пробуждение NAS (WOL)
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .env-example # Пример файла переменных окружения
-├── .gitignore # Файл игнорирования Git
-├── .dockerignore # Файлы, игнорируемые при сборке Docker-образа
-├── Dockerfile # Инструкции для сборки Docker-образа
-├── docker-compose.yml # Конфигурация Docker Compose
-├── deploy.sh # Скрипт развёртывания для Linux
-├── deploy.cmd # Скрипт развёртывания для Windows
-├── entrypoint.sh # Скрипт для запуска в Docker
-├── README.md # Основная документация
-├── README_DOCKER.md # Документация по Docker-развёртыванию
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830103139.md b/.history/README_20250830103139.md
deleted file mode 100644
index 473468e..0000000
--- a/.history/README_20250830103139.md
+++ /dev/null
@@ -1,236 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-- ✅ Список активных процессов
-- ✅ Мониторинг сетевых подключений
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-- ✅ Просмотр файлов и папок
-- ✅ Поиск файлов
-- ✅ Мониторинг квот пользователей
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-### Дополнительные функции
-- ✅ Мониторинг обновлений DSM и пакетов
-- ✅ Управление расписанием питания
-- ✅ Проверка статуса резервного копирования
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-- Доступ к порту API Synology DSM (обычно 5000 или 5001)
-
-## Установка и настройка
-
-### Метод 1: Локальный запуск
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python run.py
- ```
-
-### Метод 2: Docker
-
-1. Убедитесь, что Docker и Docker Compose установлены в вашей системе.
-
-2. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-3. Создайте файл `.env` на основе `.env-example` и заполните необходимыми значениями.
-
-4. Запустите скрипт развертывания:
- ```bash
- # Linux/macOS
- chmod +x deploy.sh
- ./deploy.sh
-
- # Windows
- deploy.cmd
- ```
-
- Или запустите вручную:
- ```bash
- docker-compose up -d --build
- ```
-
-5. Проверьте статус:
- ```bash
- docker-compose ps
- ```
-
-6. Просмотр логов:
- ```bash
- docker-compose logs -f
- ```
-
-Дополнительная информация о Docker-развертывании доступна в файле [README_DOCKER.md](README_DOCKER.md).
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-### Основные команды
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS (включение, выключение, перезагрузка)
-- `/help` - Вывод справочной информации
-
-### Информационные команды
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-- `/temperature` - Температура устройства
-- `/processes` - Список активных процессов
-- `/network` - Сетевая информация
-
-### Расширенные команды
-- `/schedule` - Расписание питания
-- `/browse` - Просмотр файлов
-- `/search <запрос>` - Поиск файлов
-- `/updates` - Проверка обновлений
-- `/backup` - Статус резервного копирования
-- `/quota` - Квоты пользователей
-
-### Быстрые команды
-- `/quickreboot` - Быстрая перезагрузка
-- `/wakeup` - Пробуждение NAS (WOL)
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .env-example # Пример файла переменных окружения
-├── .gitignore # Файл игнорирования Git
-├── .dockerignore # Файлы, игнорируемые при сборке Docker-образа
-├── Dockerfile # Инструкции для сборки Docker-образа
-├── docker-compose.yml # Конфигурация Docker Compose
-├── deploy.sh # Скрипт развёртывания для Linux
-├── deploy.cmd # Скрипт развёртывания для Windows
-├── entrypoint.sh # Скрипт для запуска в Docker
-├── README.md # Основная документация
-├── README_DOCKER.md # Документация по Docker-развёртыванию
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Устранение неисправностей
-
-### Проблемы с подключением к Synology NAS
-
-1. Убедитесь, что NAS доступен по сети (можно проверить с помощью команды `ping`).
-2. Проверьте правильность логина и пароля в `.env`.
-3. Убедитесь, что DSM API включено в настройках NAS.
-
-### Проблемы с Docker
-
-1. Проверьте статус контейнера: `docker-compose ps`
-2. Просмотрите логи: `docker-compose logs -f`
-3. Перезапустите контейнер: `docker-compose restart`
-4. Проверьте состояние здоровья: `docker inspect --format="{{json .State.Health}}" synology-power-control-bot`
-5. Проверьте, что все переменные окружения корректно переданы в контейнер.
-
-### Обновление в Docker
-
-Для обновления бота в Docker:
-
-1. Остановите контейнеры:
- ```bash
- docker-compose down
- ```
-
-2. Загрузите обновления (если используете Git):
- ```bash
- git pull
- ```
-
-3. Запустите контейнеры заново:
- ```bash
- docker-compose up -d --build
- ```
-
-## Лицензия
-
-MIT
diff --git a/.history/README_20250830103154.md b/.history/README_20250830103154.md
deleted file mode 100644
index 473468e..0000000
--- a/.history/README_20250830103154.md
+++ /dev/null
@@ -1,236 +0,0 @@
-# Synology Power Control Bot
-
-Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели).
-
-## Возможности
-
-### Управление питанием
-- ✅ Включение питания через Wake-on-LAN
-- ✅ Выключение питания через API DSM
-- ✅ Перезагрузка системы с отслеживанием статуса
-
-### Мониторинг системы
-- ✅ Проверка онлайн статуса NAS
-- ✅ Информация о системе (модель, версия DSM, время работы)
-- ✅ Мониторинг загрузки CPU и памяти
-- ✅ Данные о температуре и сетевой активности
-- ✅ Статус хранилища и дисков
-- ✅ Информация о безопасности системы
-- ✅ Список активных процессов
-- ✅ Мониторинг сетевых подключений
-
-### Управление данными
-- ✅ Просмотр списка общих папок
-- ✅ Информация о томах и дисках
-- ✅ Статистика использования дисков
-- ✅ Просмотр файлов и папок
-- ✅ Поиск файлов
-- ✅ Мониторинг квот пользователей
-
-### Безопасность
-- ✅ Ограничение доступа по ID пользователей Telegram
-- ✅ Безопасное хранение учетных данных
-
-### Дополнительные функции
-- ✅ Мониторинг обновлений DSM и пакетов
-- ✅ Управление расписанием питания
-- ✅ Проверка статуса резервного копирования
-
-## Требования
-
-- Python 3.8+
-- Synology NAS с включенным WoL
-- Учетная запись администратора DSM
-- Telegram Bot API Token
-- Доступ к порту API Synology DSM (обычно 5000 или 5001)
-
-## Установка и настройка
-
-### Метод 1: Локальный запуск
-
-1. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-2. Установите зависимости:
- ```bash
- pip install -r requirements.txt
- ```
-
-3. Настройте параметры в файле `.env`:
- ```
- # Telegram Bot Configuration
- TELEGRAM_TOKEN=your_telegram_bot_token
- ADMIN_USER_IDS=123456789,987654321
-
- # Synology NAS Configuration
- SYNOLOGY_HOST=192.168.1.100
- SYNOLOGY_PORT=5000
- SYNOLOGY_USERNAME=admin
- SYNOLOGY_PASSWORD=your_password
- SYNOLOGY_SECURE=False
- SYNOLOGY_TIMEOUT=10
-
- # Wake-on-LAN Configuration
- SYNOLOGY_MAC=00:11:22:33:44:55
- WOL_PORT=9
- ```
-
-4. Запустите бота:
- ```bash
- python run.py
- ```
-
-### Метод 2: Docker
-
-1. Убедитесь, что Docker и Docker Compose установлены в вашей системе.
-
-2. Клонируйте репозиторий:
- ```bash
- git clone https://github.com/yourusername/synology_power_control_bot.git
- cd synology_power_control_bot
- ```
-
-3. Создайте файл `.env` на основе `.env-example` и заполните необходимыми значениями.
-
-4. Запустите скрипт развертывания:
- ```bash
- # Linux/macOS
- chmod +x deploy.sh
- ./deploy.sh
-
- # Windows
- deploy.cmd
- ```
-
- Или запустите вручную:
- ```bash
- docker-compose up -d --build
- ```
-
-5. Проверьте статус:
- ```bash
- docker-compose ps
- ```
-
-6. Просмотр логов:
- ```bash
- docker-compose logs -f
- ```
-
-Дополнительная информация о Docker-развертывании доступна в файле [README_DOCKER.md](README_DOCKER.md).
-
-## Подготовка Synology NAS
-
-1. Включите Wake-on-LAN в настройках DSM:
- - Панель управления > Сеть > Общие > Wake-on-LAN
-
-2. Убедитесь, что API DSM включено:
- - Панель управления > Службы терминала и SNMP > Включить DSM API
-
-3. Узнайте MAC-адрес вашего NAS:
- - Панель управления > Сеть > Сетевой интерфейс
-
-## Команды бота
-
-### Основные команды
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS (включение, выключение, перезагрузка)
-- `/help` - Вывод справочной информации
-
-### Информационные команды
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-- `/temperature` - Температура устройства
-- `/processes` - Список активных процессов
-- `/network` - Сетевая информация
-
-### Расширенные команды
-- `/schedule` - Расписание питания
-- `/browse` - Просмотр файлов
-- `/search <запрос>` - Поиск файлов
-- `/updates` - Проверка обновлений
-- `/backup` - Статус резервного копирования
-- `/quota` - Квоты пользователей
-
-### Быстрые команды
-- `/quickreboot` - Быстрая перезагрузка
-- `/wakeup` - Пробуждение NAS (WOL)
-
-## Структура проекта
-
-```
-synology_power_control_bot/
-├── logs/ # Директория для логов
-├── src/ # Исходный код
-│ ├── api/ # Модули для работы с API
-│ │ └── synology.py # API для работы с Synology NAS
-│ ├── config/ # Модули конфигурации
-│ │ └── config.py # Основная конфигурация
-│ ├── handlers/ # Обработчики команд бота
-│ │ └── command_handlers.py
-│ ├── utils/ # Вспомогательные утилиты
-│ │ └── logger.py # Настройка логирования
-│ └── bot.py # Основной файл запуска бота
-├── .env # Файл с переменными окружения
-├── .env-example # Пример файла переменных окружения
-├── .gitignore # Файл игнорирования Git
-├── .dockerignore # Файлы, игнорируемые при сборке Docker-образа
-├── Dockerfile # Инструкции для сборки Docker-образа
-├── docker-compose.yml # Конфигурация Docker Compose
-├── deploy.sh # Скрипт развёртывания для Linux
-├── deploy.cmd # Скрипт развёртывания для Windows
-├── entrypoint.sh # Скрипт для запуска в Docker
-├── README.md # Основная документация
-├── README_DOCKER.md # Документация по Docker-развёртыванию
-└── requirements.txt # Зависимости проекта
-```
-
-## Безопасность
-
-Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`.
-
-## Устранение неисправностей
-
-### Проблемы с подключением к Synology NAS
-
-1. Убедитесь, что NAS доступен по сети (можно проверить с помощью команды `ping`).
-2. Проверьте правильность логина и пароля в `.env`.
-3. Убедитесь, что DSM API включено в настройках NAS.
-
-### Проблемы с Docker
-
-1. Проверьте статус контейнера: `docker-compose ps`
-2. Просмотрите логи: `docker-compose logs -f`
-3. Перезапустите контейнер: `docker-compose restart`
-4. Проверьте состояние здоровья: `docker inspect --format="{{json .State.Health}}" synology-power-control-bot`
-5. Проверьте, что все переменные окружения корректно переданы в контейнер.
-
-### Обновление в Docker
-
-Для обновления бота в Docker:
-
-1. Остановите контейнеры:
- ```bash
- docker-compose down
- ```
-
-2. Загрузите обновления (если используете Git):
- ```bash
- git pull
- ```
-
-3. Запустите контейнеры заново:
- ```bash
- docker-compose up -d --build
- ```
-
-## Лицензия
-
-MIT
diff --git a/.history/README_DOCKER_20250830102736.md b/.history/README_DOCKER_20250830102736.md
deleted file mode 100644
index 6bd6f3f..0000000
--- a/.history/README_DOCKER_20250830102736.md
+++ /dev/null
@@ -1,106 +0,0 @@
-# Synology Power Control Bot - Docker Deployment
-
-## Подготовка к развертыванию
-
-Перед развертыванием в Docker убедитесь, что:
-
-1. Docker и Docker Compose установлены в вашей системе.
-2. Файл `.env` настроен с правильными значениями.
-
-## Структура проекта для Docker
-
-```
-synology_power_control_bot/
-├── src/ # Исходный код бота
-├── logs/ # Папка для логов (будет смонтирована как том)
-├── .env # Файл с переменными окружения
-├── requirements.txt # Зависимости Python
-├── Dockerfile # Инструкции для сборки образа
-├── docker-compose.yml # Конфигурация Docker Compose
-└── run.py # Точка входа
-```
-
-## Настройка переменных окружения
-
-Убедитесь, что файл `.env` содержит все необходимые переменные:
-
-```
-# Telegram Bot API
-TELEGRAM_TOKEN=your_telegram_bot_token
-ADMIN_USER_IDS=123456789,987654321 # ID пользователей-администраторов через запятую
-
-# Synology NAS
-SYNOLOGY_HOST=192.168.1.100
-SYNOLOGY_PORT=5000 # Обычно 5000 для HTTP и 5001 для HTTPS
-SYNOLOGY_USERNAME=your_username
-SYNOLOGY_PASSWORD=your_password
-SYNOLOGY_SECURE=True # Использовать HTTPS
-SYNOLOGY_VERIFY_SSL=False # Проверка SSL-сертификата
-SYNOLOGY_TIMEOUT=10 # Таймаут для API запросов в секундах
-SYNOLOGY_API_VERSION=1 # Версия API
-SYNOLOGY_POWER_API=SYNO.Core.System # API для управления питанием
-
-# WOL (Wake-on-LAN)
-MAC_ADDRESS=00:11:22:33:44:55 # MAC-адрес Synology NAS
-WOL_BROADCAST=255.255.255.255 # Broadcast-адрес для WOL
-WOL_PORT=9 # Порт для WOL (обычно 7 или 9)
-
-# Logging
-LOG_LEVEL=INFO
-```
-
-## Сборка и запуск
-
-### Сборка и запуск контейнеров
-
-```bash
-docker-compose up -d --build
-```
-
-### Просмотр логов
-
-```bash
-docker-compose logs -f
-```
-
-### Остановка контейнеров
-
-```bash
-docker-compose down
-```
-
-## Обновление
-
-Для обновления бота:
-
-1. Остановите контейнеры:
- ```bash
- docker-compose down
- ```
-
-2. Скачайте последние изменения (если используете Git):
- ```bash
- git pull
- ```
-
-3. Соберите и запустите контейнеры заново:
- ```bash
- docker-compose up -d --build
- ```
-
-## Устранение неполадок
-
-### Проверка статуса контейнера
-```bash
-docker-compose ps
-```
-
-### Проверка логов контейнера
-```bash
-docker-compose logs -f synology-bot
-```
-
-### Подключение к контейнеру
-```bash
-docker-compose exec synology-bot bash
-```
diff --git a/.history/README_DOCKER_20250830103154.md b/.history/README_DOCKER_20250830103154.md
deleted file mode 100644
index 6bd6f3f..0000000
--- a/.history/README_DOCKER_20250830103154.md
+++ /dev/null
@@ -1,106 +0,0 @@
-# Synology Power Control Bot - Docker Deployment
-
-## Подготовка к развертыванию
-
-Перед развертыванием в Docker убедитесь, что:
-
-1. Docker и Docker Compose установлены в вашей системе.
-2. Файл `.env` настроен с правильными значениями.
-
-## Структура проекта для Docker
-
-```
-synology_power_control_bot/
-├── src/ # Исходный код бота
-├── logs/ # Папка для логов (будет смонтирована как том)
-├── .env # Файл с переменными окружения
-├── requirements.txt # Зависимости Python
-├── Dockerfile # Инструкции для сборки образа
-├── docker-compose.yml # Конфигурация Docker Compose
-└── run.py # Точка входа
-```
-
-## Настройка переменных окружения
-
-Убедитесь, что файл `.env` содержит все необходимые переменные:
-
-```
-# Telegram Bot API
-TELEGRAM_TOKEN=your_telegram_bot_token
-ADMIN_USER_IDS=123456789,987654321 # ID пользователей-администраторов через запятую
-
-# Synology NAS
-SYNOLOGY_HOST=192.168.1.100
-SYNOLOGY_PORT=5000 # Обычно 5000 для HTTP и 5001 для HTTPS
-SYNOLOGY_USERNAME=your_username
-SYNOLOGY_PASSWORD=your_password
-SYNOLOGY_SECURE=True # Использовать HTTPS
-SYNOLOGY_VERIFY_SSL=False # Проверка SSL-сертификата
-SYNOLOGY_TIMEOUT=10 # Таймаут для API запросов в секундах
-SYNOLOGY_API_VERSION=1 # Версия API
-SYNOLOGY_POWER_API=SYNO.Core.System # API для управления питанием
-
-# WOL (Wake-on-LAN)
-MAC_ADDRESS=00:11:22:33:44:55 # MAC-адрес Synology NAS
-WOL_BROADCAST=255.255.255.255 # Broadcast-адрес для WOL
-WOL_PORT=9 # Порт для WOL (обычно 7 или 9)
-
-# Logging
-LOG_LEVEL=INFO
-```
-
-## Сборка и запуск
-
-### Сборка и запуск контейнеров
-
-```bash
-docker-compose up -d --build
-```
-
-### Просмотр логов
-
-```bash
-docker-compose logs -f
-```
-
-### Остановка контейнеров
-
-```bash
-docker-compose down
-```
-
-## Обновление
-
-Для обновления бота:
-
-1. Остановите контейнеры:
- ```bash
- docker-compose down
- ```
-
-2. Скачайте последние изменения (если используете Git):
- ```bash
- git pull
- ```
-
-3. Соберите и запустите контейнеры заново:
- ```bash
- docker-compose up -d --build
- ```
-
-## Устранение неполадок
-
-### Проверка статуса контейнера
-```bash
-docker-compose ps
-```
-
-### Проверка логов контейнера
-```bash
-docker-compose logs -f synology-bot
-```
-
-### Подключение к контейнеру
-```bash
-docker-compose exec synology-bot bash
-```
diff --git a/.history/deploy_20250830102934.sh b/.history/deploy_20250830102934.sh
deleted file mode 100644
index fc7e1aa..0000000
--- a/.history/deploy_20250830102934.sh
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/bin/bash
-# deploy.sh - Скрипт для развертывания Synology Power Control Bot
-
-# Цвета для вывода
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-RED='\033[0;31m'
-NC='\033[0m' # No Color
-
-# Проверяем наличие Docker и Docker Compose
-echo -e "${YELLOW}Проверка наличия Docker...${NC}"
-if ! [ -x "$(command -v docker)" ]; then
- echo -e "${RED}Ошибка: Docker не установлен.${NC}" >&2
- echo -e "Установите Docker, следуя инструкциям: https://docs.docker.com/get-docker/"
- exit 1
-fi
-
-echo -e "${YELLOW}Проверка наличия Docker Compose...${NC}"
-if ! [ -x "$(command -v docker-compose)" ] && ! [ -x "$(command -v docker compose)" ]; then
- echo -e "${RED}Ошибка: Docker Compose не установлен.${NC}" >&2
- echo -e "Установите Docker Compose, следуя инструкциям: https://docs.docker.com/compose/install/"
- exit 1
-fi
-
-# Проверяем наличие файла .env
-echo -e "${YELLOW}Проверка файла .env...${NC}"
-if [ ! -f ".env" ]; then
- echo -e "${RED}Ошибка: Файл .env не найден.${NC}" >&2
- echo -e "Создайте файл .env с необходимыми переменными окружения."
- exit 1
-fi
-
-# Создаем директорию для логов
-echo -e "${YELLOW}Создание директории для логов...${NC}"
-mkdir -p logs
-chmod 777 logs
-
-# Сборка и запуск Docker контейнеров
-echo -e "${YELLOW}Сборка и запуск Docker контейнеров...${NC}"
-docker-compose down
-docker-compose up -d --build
-
-# Проверка статуса контейнеров
-echo -e "${YELLOW}Проверка статуса контейнеров...${NC}"
-docker-compose ps
-
-echo -e "${GREEN}Развертывание завершено успешно!${NC}"
-echo -e "Для просмотра логов: ${YELLOW}docker-compose logs -f${NC}"
-echo -e "Для остановки: ${YELLOW}docker-compose down${NC}"
diff --git a/.history/deploy_20250830102949.cmd b/.history/deploy_20250830102949.cmd
deleted file mode 100644
index 6ded549..0000000
--- a/.history/deploy_20250830102949.cmd
+++ /dev/null
@@ -1,45 +0,0 @@
-@echo off
-REM deploy.cmd - Скрипт для развертывания Synology Power Control Bot на Windows
-
-echo Проверка наличия Docker...
-where docker >nul 2>&1
-if %ERRORLEVEL% NEQ 0 (
- echo Ошибка: Docker не установлен.
- echo Установите Docker Desktop, следуя инструкциям: https://docs.docker.com/desktop/windows/install/
- exit /b 1
-)
-
-echo Проверка наличия Docker Compose...
-where docker-compose >nul 2>&1
-if %ERRORLEVEL% NEQ 0 (
- docker compose version >nul 2>&1
- if %ERRORLEVEL% NEQ 0 (
- echo Ошибка: Docker Compose не установлен.
- echo Установите Docker Desktop, следуя инструкциям: https://docs.docker.com/desktop/windows/install/
- exit /b 1
- )
-)
-
-echo Проверка файла .env...
-if not exist .env (
- echo Ошибка: Файл .env не найден.
- echo Создайте файл .env с необходимыми переменными окружения.
- exit /b 1
-)
-
-echo Создание директории для логов...
-if not exist logs mkdir logs
-
-echo Сборка и запуск Docker контейнеров...
-docker-compose down
-docker-compose up -d --build
-
-echo Проверка статуса контейнеров...
-docker-compose ps
-
-echo.
-echo Развертывание завершено успешно!
-echo Для просмотра логов: docker-compose logs -f
-echo Для остановки: docker-compose down
-
-pause
diff --git a/.history/deploy_20250830103154.cmd b/.history/deploy_20250830103154.cmd
deleted file mode 100644
index 6ded549..0000000
--- a/.history/deploy_20250830103154.cmd
+++ /dev/null
@@ -1,45 +0,0 @@
-@echo off
-REM deploy.cmd - Скрипт для развертывания Synology Power Control Bot на Windows
-
-echo Проверка наличия Docker...
-where docker >nul 2>&1
-if %ERRORLEVEL% NEQ 0 (
- echo Ошибка: Docker не установлен.
- echo Установите Docker Desktop, следуя инструкциям: https://docs.docker.com/desktop/windows/install/
- exit /b 1
-)
-
-echo Проверка наличия Docker Compose...
-where docker-compose >nul 2>&1
-if %ERRORLEVEL% NEQ 0 (
- docker compose version >nul 2>&1
- if %ERRORLEVEL% NEQ 0 (
- echo Ошибка: Docker Compose не установлен.
- echo Установите Docker Desktop, следуя инструкциям: https://docs.docker.com/desktop/windows/install/
- exit /b 1
- )
-)
-
-echo Проверка файла .env...
-if not exist .env (
- echo Ошибка: Файл .env не найден.
- echo Создайте файл .env с необходимыми переменными окружения.
- exit /b 1
-)
-
-echo Создание директории для логов...
-if not exist logs mkdir logs
-
-echo Сборка и запуск Docker контейнеров...
-docker-compose down
-docker-compose up -d --build
-
-echo Проверка статуса контейнеров...
-docker-compose ps
-
-echo.
-echo Развертывание завершено успешно!
-echo Для просмотра логов: docker-compose logs -f
-echo Для остановки: docker-compose down
-
-pause
diff --git a/.history/deploy_20250830103154.sh b/.history/deploy_20250830103154.sh
deleted file mode 100644
index fc7e1aa..0000000
--- a/.history/deploy_20250830103154.sh
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/bin/bash
-# deploy.sh - Скрипт для развертывания Synology Power Control Bot
-
-# Цвета для вывода
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-RED='\033[0;31m'
-NC='\033[0m' # No Color
-
-# Проверяем наличие Docker и Docker Compose
-echo -e "${YELLOW}Проверка наличия Docker...${NC}"
-if ! [ -x "$(command -v docker)" ]; then
- echo -e "${RED}Ошибка: Docker не установлен.${NC}" >&2
- echo -e "Установите Docker, следуя инструкциям: https://docs.docker.com/get-docker/"
- exit 1
-fi
-
-echo -e "${YELLOW}Проверка наличия Docker Compose...${NC}"
-if ! [ -x "$(command -v docker-compose)" ] && ! [ -x "$(command -v docker compose)" ]; then
- echo -e "${RED}Ошибка: Docker Compose не установлен.${NC}" >&2
- echo -e "Установите Docker Compose, следуя инструкциям: https://docs.docker.com/compose/install/"
- exit 1
-fi
-
-# Проверяем наличие файла .env
-echo -e "${YELLOW}Проверка файла .env...${NC}"
-if [ ! -f ".env" ]; then
- echo -e "${RED}Ошибка: Файл .env не найден.${NC}" >&2
- echo -e "Создайте файл .env с необходимыми переменными окружения."
- exit 1
-fi
-
-# Создаем директорию для логов
-echo -e "${YELLOW}Создание директории для логов...${NC}"
-mkdir -p logs
-chmod 777 logs
-
-# Сборка и запуск Docker контейнеров
-echo -e "${YELLOW}Сборка и запуск Docker контейнеров...${NC}"
-docker-compose down
-docker-compose up -d --build
-
-# Проверка статуса контейнеров
-echo -e "${YELLOW}Проверка статуса контейнеров...${NC}"
-docker-compose ps
-
-echo -e "${GREEN}Развертывание завершено успешно!${NC}"
-echo -e "Для просмотра логов: ${YELLOW}docker-compose logs -f${NC}"
-echo -e "Для остановки: ${YELLOW}docker-compose down${NC}"
diff --git a/.history/diagnose_api_20250830081925.py b/.history/diagnose_api_20250830081925.py
deleted file mode 100644
index 2ae4b6a..0000000
--- a/.history/diagnose_api_20250830081925.py
+++ /dev/null
@@ -1,91 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Диагностический скрипт для определения совместимых API
-"""
-
-import os
-import sys
-import logging
-import argparse
-from pathlib import Path
-
-# Добавляем родительскую директорию в sys.path
-parent_dir = str(Path(__file__).resolve().parent.parent)
-if parent_dir not in sys.path:
- sys.path.insert(0, parent_dir)
-
-from src.api.api_discovery import discover_available_apis
-from src.config.config import SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_SECURE
-
-# Настройка логгера
-logging.basicConfig(
- level=logging.DEBUG,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-def main():
- """Точка входа для диагностического скрипта"""
- parser = argparse.ArgumentParser(description='Synology API Diagnostic Tool')
- parser.add_argument('--host', help='Synology host address', default=SYNOLOGY_HOST)
- parser.add_argument('--port', type=int, help='Synology host port', default=SYNOLOGY_PORT)
- parser.add_argument('--secure', action='store_true', help='Use HTTPS', default=SYNOLOGY_SECURE)
-
- args = parser.parse_args()
-
- protocol = "https" if args.secure else "http"
- base_url = f"{protocol}://{args.host}:{args.port}/webapi"
-
- print(f"Scanning APIs at {base_url}...")
-
- apis = discover_available_apis(base_url)
-
- if not apis:
- print("No APIs were discovered. Check connection parameters.")
- return
-
- print(f"Discovered {len(apis)} APIs")
-
- # Анализ результатов
-
- # 1. Ищем API для управления питанием
- print("\nPower Management APIs:")
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- for api in power_apis:
- info = apis[api]
- print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}")
-
- # 2. Ищем API для информации о системе
- print("\nSystem Information APIs:")
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- for api in system_info_apis:
- info = apis[api]
- print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}")
-
- # 3. Ищем API для перезагрузки
- print("\nReboot/Restart APIs:")
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
- for api in reboot_apis:
- info = apis[api]
- print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}")
-
- print("\nRecommended API Settings:")
-
- if power_apis:
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1))
- print(f"Power API: {recommended_power_api}, version: {apis[recommended_power_api].get('maxVersion', 1)}")
- else:
- print("Power API: Not found, falling back to SYNO.Core.System")
-
- if system_info_apis:
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1))
- print(f"System Info API: {recommended_info_api}, version: {apis[recommended_info_api].get('maxVersion', 1)}")
- else:
- print("System Info API: Not found, falling back to SYNO.DSM.Info")
-
- print("\nThese settings should be added to your .env file.")
-
-if __name__ == "__main__":
- main()
diff --git a/.history/diagnose_api_20250830081957.py b/.history/diagnose_api_20250830081957.py
deleted file mode 100644
index 2ae4b6a..0000000
--- a/.history/diagnose_api_20250830081957.py
+++ /dev/null
@@ -1,91 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Диагностический скрипт для определения совместимых API
-"""
-
-import os
-import sys
-import logging
-import argparse
-from pathlib import Path
-
-# Добавляем родительскую директорию в sys.path
-parent_dir = str(Path(__file__).resolve().parent.parent)
-if parent_dir not in sys.path:
- sys.path.insert(0, parent_dir)
-
-from src.api.api_discovery import discover_available_apis
-from src.config.config import SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_SECURE
-
-# Настройка логгера
-logging.basicConfig(
- level=logging.DEBUG,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-def main():
- """Точка входа для диагностического скрипта"""
- parser = argparse.ArgumentParser(description='Synology API Diagnostic Tool')
- parser.add_argument('--host', help='Synology host address', default=SYNOLOGY_HOST)
- parser.add_argument('--port', type=int, help='Synology host port', default=SYNOLOGY_PORT)
- parser.add_argument('--secure', action='store_true', help='Use HTTPS', default=SYNOLOGY_SECURE)
-
- args = parser.parse_args()
-
- protocol = "https" if args.secure else "http"
- base_url = f"{protocol}://{args.host}:{args.port}/webapi"
-
- print(f"Scanning APIs at {base_url}...")
-
- apis = discover_available_apis(base_url)
-
- if not apis:
- print("No APIs were discovered. Check connection parameters.")
- return
-
- print(f"Discovered {len(apis)} APIs")
-
- # Анализ результатов
-
- # 1. Ищем API для управления питанием
- print("\nPower Management APIs:")
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- for api in power_apis:
- info = apis[api]
- print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}")
-
- # 2. Ищем API для информации о системе
- print("\nSystem Information APIs:")
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- for api in system_info_apis:
- info = apis[api]
- print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}")
-
- # 3. Ищем API для перезагрузки
- print("\nReboot/Restart APIs:")
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
- for api in reboot_apis:
- info = apis[api]
- print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}")
-
- print("\nRecommended API Settings:")
-
- if power_apis:
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1))
- print(f"Power API: {recommended_power_api}, version: {apis[recommended_power_api].get('maxVersion', 1)}")
- else:
- print("Power API: Not found, falling back to SYNO.Core.System")
-
- if system_info_apis:
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1))
- print(f"System Info API: {recommended_info_api}, version: {apis[recommended_info_api].get('maxVersion', 1)}")
- else:
- print("System Info API: Not found, falling back to SYNO.DSM.Info")
-
- print("\nThese settings should be added to your .env file.")
-
-if __name__ == "__main__":
- main()
diff --git a/.history/direct_api_test_20250830084231.py b/.history/direct_api_test_20250830084231.py
deleted file mode 100644
index ca9c245..0000000
--- a/.history/direct_api_test_20250830084231.py
+++ /dev/null
@@ -1,293 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Тестовый скрипт для прямого доступа к API Synology для получения информации о системе.
-Используется для отладки и определения совместимых API.
-"""
-
-import requests
-import logging
-import json
-import sys
-import os
-import urllib3
-from requests.adapters import HTTPAdapter
-from urllib3.util import Retry
-
-# Добавляем корневой каталог в путь для импорта
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-# Настройка логирования
-logging.basicConfig(
- level=logging.DEBUG,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-def direct_api_test():
- """Прямой тест API без использования классов для определения проблемы"""
- # Создаем базовую сессию
- session = requests.Session()
- session.verify = False # Отключаем проверку SSL
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=3,
- status_forcelist=[429, 500, 502, 503, 504],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.0
- )
- adapter = HTTPAdapter(max_retries=retry_strategy)
- session.mount("http://", adapter)
- session.mount("https://", adapter)
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- logger.info(f"Тестирование прямого API доступа к {base_url}")
-
- # Шаг 1: Авторизация
- logger.info("Шаг 1: Попытка авторизации...")
-
- # Сначала получаем информацию об API авторизации
- api_info_url = f"{base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- try:
- auth_info_response = session.get(api_info_url, params=api_info_params, timeout=10)
- auth_info_data = auth_info_response.json()
-
- if auth_info_data.get("success"):
- auth_info = auth_info_data.get("data", {}).get("SYNO.API.Auth", {})
- auth_path = auth_info.get("path", "auth.cgi")
- auth_max_version = auth_info.get("maxVersion", 6)
-
- logger.info(f"API авторизации: путь={auth_path}, макс. версия={auth_max_version}")
-
- # Пробуем версию 6 или максимальную доступную
- auth_version = min(6, auth_max_version)
-
- # Выполняем авторизацию
- auth_url = f"{base_url}/{auth_path}"
- auth_params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "DirectApiTest",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- auth_params["enable_syno_token"] = "yes"
-
- logger.info(f"Авторизация с использованием SYNO.API.Auth v{auth_version}")
- auth_response = session.get(auth_url, params=auth_params, timeout=10)
- auth_data = auth_response.json()
-
- if auth_data.get("success"):
- sid = auth_data.get("data", {}).get("sid")
- logger.info(f"Авторизация успешна! SID: {sid[:10]}...")
-
- # Шаг 2: Тестирование различных API для получения информации о системе
- logger.info("Шаг 2: Тестирование различных API для получения информации о системе")
-
- # Создаем список API для тестирования
- api_to_test = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 1},
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System", "method": "info", "version": 2},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 2},
- {"name": "SYNO.Core.System.Utilization", "method": "get", "version": 1},
- {"name": "SYNO.Core.CurrentConnection", "method": "list", "version": 1}
- ]
-
- # Перебираем все API и тестируем их
- for api in api_to_test:
- # Сначала получаем информацию о конкретном API
- try:
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api["name"]
- }
-
- api_info_resp = session.get(api_info_url, params=api_info_params, timeout=10)
- api_info_data = api_info_resp.json()
-
- if api_info_data.get("success") and api["name"] in api_info_data.get("data", {}):
- api_details = api_info_data["data"][api["name"]]
- api_path = api_details.get("path", "entry.cgi")
- api_min_version = api_details.get("minVersion", 1)
- api_max_version = api_details.get("maxVersion", 1)
-
- # Проверяем, поддерживается ли указанная версия
- if api["version"] < api_min_version:
- logger.warning(f"{api['name']} v{api['version']} ниже минимальной {api_min_version}, используем {api_min_version}")
- test_version = api_min_version
- elif api["version"] > api_max_version:
- logger.warning(f"{api['name']} v{api['version']} выше максимальной {api_max_version}, используем {api_max_version}")
- test_version = api_max_version
- else:
- test_version = api["version"]
-
- # Выполняем запрос API
- test_url = f"{base_url}/{api_path}"
- test_params = {
- "api": api["name"],
- "version": str(test_version),
- "method": api["method"],
- "_sid": sid # Используем sid для аутентификации
- }
-
- logger.info(f"Тестирование {api['name']}.{api['method']} v{test_version}")
- test_response = session.get(test_url, params=test_params, timeout=10)
- test_data = test_response.json()
-
- if test_data.get("success"):
- logger.info(f"API {api['name']}.{api['method']} v{test_version} РАБОТАЕТ!")
- logger.info(f"Результат: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...")
- else:
- error_code = test_data.get("error", {}).get("code", -1)
- logger.error(f"API {api['name']}.{api['method']} v{test_version} ОШИБКА: {error_code}")
-
- # Если ошибка связана с сессией, попробуем еще раз авторизоваться
- if error_code == 119: # Session timeout
- logger.info("Повторная авторизация из-за ошибки 119...")
-
- # Создаем новую сессию
- new_session = requests.Session()
- new_session.verify = False
-
- auth_response = new_session.get(auth_url, params=auth_params, timeout=10)
- auth_data = auth_response.json()
-
- if auth_data.get("success"):
- new_sid = auth_data.get("data", {}).get("sid")
- logger.info(f"Повторная авторизация успешна! Новый SID: {new_sid[:10]}...")
-
- # Пробуем запрос с новым SID
- test_params["_sid"] = new_sid
- logger.info(f"Повторное тестирование {api['name']}.{api['method']} v{test_version}")
- test_response = new_session.get(test_url, params=test_params, timeout=10)
- test_data = test_response.json()
-
- if test_data.get("success"):
- logger.info(f"API {api['name']}.{api['method']} v{test_version} теперь РАБОТАЕТ!")
- logger.info(f"Результат с новой сессией: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...")
- else:
- error_code = test_data.get("error", {}).get("code", -1)
- logger.error(f"API {api['name']}.{api['method']} v{test_version} ВСЕ ЕЩЕ С ОШИБКОЙ: {error_code}")
- else:
- logger.warning(f"API {api['name']} не найден в информации API")
-
- except Exception as e:
- logger.error(f"Ошибка при тестировании {api['name']}.{api['method']} v{api['version']}: {str(e)}")
-
- # Шаг 3: Тестирование комбинации запросов для решения проблемы
- logger.info("Шаг 3: Тестирование комбинации запросов для решения проблемы")
-
- # Создаем новую сессию для каждого запроса
- for api in [{"name": "SYNO.DSM.Info", "method": "getinfo", "version": 1}]:
- try:
- fresh_session = requests.Session()
- fresh_session.verify = False
-
- # Авторизуемся
- auth_response = fresh_session.get(auth_url, params=auth_params, timeout=10)
- auth_data = auth_response.json()
-
- if auth_data.get("success"):
- fresh_sid = auth_data.get("data", {}).get("sid")
- logger.info(f"Авторизация в новой сессии успешна! SID: {fresh_sid[:10]}...")
-
- # Сразу же делаем запрос для получения информации в той же сессии
- test_params = {
- "api": api["name"],
- "version": str(api["version"]),
- "method": api["method"],
- "_sid": fresh_sid
- }
-
- test_url = f"{base_url}/entry.cgi" # Используем entry.cgi по умолчанию
- logger.info(f"Тест в свежей сессии: {api['name']}.{api['method']} v{api['version']}")
- test_response = fresh_session.get(test_url, params=test_params, timeout=10)
- test_data = test_response.json()
-
- if test_data.get("success"):
- logger.info(f"API в свежей сессии РАБОТАЕТ!")
- logger.info(f"Результат: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...")
- else:
- error_code = test_data.get("error", {}).get("code", -1)
- logger.error(f"API в свежей сессии ОШИБКА: {error_code}")
- except Exception as e:
- logger.error(f"Ошибка при тестировании свежей сессии: {str(e)}")
-
- # Шаг 4: Получаем информацию об остальных API
- logger.info("Шаг 4: Получаем информацию о доступных API для уточнения проблемы")
-
- # Запрашиваем все API из SYNO.API.Info
- try:
- all_api_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "all"
- }
-
- all_api_response = session.get(api_info_url, params=all_api_params, timeout=15) # Больший таймаут для большого ответа
- all_api_data = all_api_response.json()
-
- if all_api_data.get("success"):
- api_list = all_api_data.get("data", {})
- logger.info(f"Получен список всех API. Найдено {len(api_list)} API.")
-
- # Ищем интересующие нас API для отладки
- interested_in = ["SYNO.DSM.Info", "SYNO.Core.System", "SYNO.Core.Hardware",
- "SYNO.Core.System.Status", "SYNO.API.Auth"]
-
- logger.info("Информация о важных API:")
- for api_name in interested_in:
- if api_name in api_list:
- logger.info(f"{api_name}: {api_list[api_name]}")
- else:
- logger.warning(f"API {api_name} не найден")
- else:
- logger.error("Не удалось получить список всех API")
- except Exception as e:
- logger.error(f"Ошибка при получении списка API: {str(e)}")
-
- else:
- error_code = auth_data.get("error", {}).get("code", -1)
- logger.error(f"Авторизация не удалась! Код ошибки: {error_code}")
- else:
- logger.error("Не удалось получить информацию об API авторизации")
-
- except Exception as e:
- logger.error(f"Произошла ошибка при выполнении теста: {str(e)}")
-
-if __name__ == "__main__":
- logger.info("Запуск прямого теста API Synology")
- direct_api_test()
diff --git a/.history/direct_api_test_20250830084257.py b/.history/direct_api_test_20250830084257.py
deleted file mode 100644
index ca9c245..0000000
--- a/.history/direct_api_test_20250830084257.py
+++ /dev/null
@@ -1,293 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Тестовый скрипт для прямого доступа к API Synology для получения информации о системе.
-Используется для отладки и определения совместимых API.
-"""
-
-import requests
-import logging
-import json
-import sys
-import os
-import urllib3
-from requests.adapters import HTTPAdapter
-from urllib3.util import Retry
-
-# Добавляем корневой каталог в путь для импорта
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-# Настройка логирования
-logging.basicConfig(
- level=logging.DEBUG,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-def direct_api_test():
- """Прямой тест API без использования классов для определения проблемы"""
- # Создаем базовую сессию
- session = requests.Session()
- session.verify = False # Отключаем проверку SSL
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=3,
- status_forcelist=[429, 500, 502, 503, 504],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.0
- )
- adapter = HTTPAdapter(max_retries=retry_strategy)
- session.mount("http://", adapter)
- session.mount("https://", adapter)
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- logger.info(f"Тестирование прямого API доступа к {base_url}")
-
- # Шаг 1: Авторизация
- logger.info("Шаг 1: Попытка авторизации...")
-
- # Сначала получаем информацию об API авторизации
- api_info_url = f"{base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- try:
- auth_info_response = session.get(api_info_url, params=api_info_params, timeout=10)
- auth_info_data = auth_info_response.json()
-
- if auth_info_data.get("success"):
- auth_info = auth_info_data.get("data", {}).get("SYNO.API.Auth", {})
- auth_path = auth_info.get("path", "auth.cgi")
- auth_max_version = auth_info.get("maxVersion", 6)
-
- logger.info(f"API авторизации: путь={auth_path}, макс. версия={auth_max_version}")
-
- # Пробуем версию 6 или максимальную доступную
- auth_version = min(6, auth_max_version)
-
- # Выполняем авторизацию
- auth_url = f"{base_url}/{auth_path}"
- auth_params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "DirectApiTest",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- auth_params["enable_syno_token"] = "yes"
-
- logger.info(f"Авторизация с использованием SYNO.API.Auth v{auth_version}")
- auth_response = session.get(auth_url, params=auth_params, timeout=10)
- auth_data = auth_response.json()
-
- if auth_data.get("success"):
- sid = auth_data.get("data", {}).get("sid")
- logger.info(f"Авторизация успешна! SID: {sid[:10]}...")
-
- # Шаг 2: Тестирование различных API для получения информации о системе
- logger.info("Шаг 2: Тестирование различных API для получения информации о системе")
-
- # Создаем список API для тестирования
- api_to_test = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 1},
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System", "method": "info", "version": 2},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 2},
- {"name": "SYNO.Core.System.Utilization", "method": "get", "version": 1},
- {"name": "SYNO.Core.CurrentConnection", "method": "list", "version": 1}
- ]
-
- # Перебираем все API и тестируем их
- for api in api_to_test:
- # Сначала получаем информацию о конкретном API
- try:
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api["name"]
- }
-
- api_info_resp = session.get(api_info_url, params=api_info_params, timeout=10)
- api_info_data = api_info_resp.json()
-
- if api_info_data.get("success") and api["name"] in api_info_data.get("data", {}):
- api_details = api_info_data["data"][api["name"]]
- api_path = api_details.get("path", "entry.cgi")
- api_min_version = api_details.get("minVersion", 1)
- api_max_version = api_details.get("maxVersion", 1)
-
- # Проверяем, поддерживается ли указанная версия
- if api["version"] < api_min_version:
- logger.warning(f"{api['name']} v{api['version']} ниже минимальной {api_min_version}, используем {api_min_version}")
- test_version = api_min_version
- elif api["version"] > api_max_version:
- logger.warning(f"{api['name']} v{api['version']} выше максимальной {api_max_version}, используем {api_max_version}")
- test_version = api_max_version
- else:
- test_version = api["version"]
-
- # Выполняем запрос API
- test_url = f"{base_url}/{api_path}"
- test_params = {
- "api": api["name"],
- "version": str(test_version),
- "method": api["method"],
- "_sid": sid # Используем sid для аутентификации
- }
-
- logger.info(f"Тестирование {api['name']}.{api['method']} v{test_version}")
- test_response = session.get(test_url, params=test_params, timeout=10)
- test_data = test_response.json()
-
- if test_data.get("success"):
- logger.info(f"API {api['name']}.{api['method']} v{test_version} РАБОТАЕТ!")
- logger.info(f"Результат: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...")
- else:
- error_code = test_data.get("error", {}).get("code", -1)
- logger.error(f"API {api['name']}.{api['method']} v{test_version} ОШИБКА: {error_code}")
-
- # Если ошибка связана с сессией, попробуем еще раз авторизоваться
- if error_code == 119: # Session timeout
- logger.info("Повторная авторизация из-за ошибки 119...")
-
- # Создаем новую сессию
- new_session = requests.Session()
- new_session.verify = False
-
- auth_response = new_session.get(auth_url, params=auth_params, timeout=10)
- auth_data = auth_response.json()
-
- if auth_data.get("success"):
- new_sid = auth_data.get("data", {}).get("sid")
- logger.info(f"Повторная авторизация успешна! Новый SID: {new_sid[:10]}...")
-
- # Пробуем запрос с новым SID
- test_params["_sid"] = new_sid
- logger.info(f"Повторное тестирование {api['name']}.{api['method']} v{test_version}")
- test_response = new_session.get(test_url, params=test_params, timeout=10)
- test_data = test_response.json()
-
- if test_data.get("success"):
- logger.info(f"API {api['name']}.{api['method']} v{test_version} теперь РАБОТАЕТ!")
- logger.info(f"Результат с новой сессией: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...")
- else:
- error_code = test_data.get("error", {}).get("code", -1)
- logger.error(f"API {api['name']}.{api['method']} v{test_version} ВСЕ ЕЩЕ С ОШИБКОЙ: {error_code}")
- else:
- logger.warning(f"API {api['name']} не найден в информации API")
-
- except Exception as e:
- logger.error(f"Ошибка при тестировании {api['name']}.{api['method']} v{api['version']}: {str(e)}")
-
- # Шаг 3: Тестирование комбинации запросов для решения проблемы
- logger.info("Шаг 3: Тестирование комбинации запросов для решения проблемы")
-
- # Создаем новую сессию для каждого запроса
- for api in [{"name": "SYNO.DSM.Info", "method": "getinfo", "version": 1}]:
- try:
- fresh_session = requests.Session()
- fresh_session.verify = False
-
- # Авторизуемся
- auth_response = fresh_session.get(auth_url, params=auth_params, timeout=10)
- auth_data = auth_response.json()
-
- if auth_data.get("success"):
- fresh_sid = auth_data.get("data", {}).get("sid")
- logger.info(f"Авторизация в новой сессии успешна! SID: {fresh_sid[:10]}...")
-
- # Сразу же делаем запрос для получения информации в той же сессии
- test_params = {
- "api": api["name"],
- "version": str(api["version"]),
- "method": api["method"],
- "_sid": fresh_sid
- }
-
- test_url = f"{base_url}/entry.cgi" # Используем entry.cgi по умолчанию
- logger.info(f"Тест в свежей сессии: {api['name']}.{api['method']} v{api['version']}")
- test_response = fresh_session.get(test_url, params=test_params, timeout=10)
- test_data = test_response.json()
-
- if test_data.get("success"):
- logger.info(f"API в свежей сессии РАБОТАЕТ!")
- logger.info(f"Результат: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...")
- else:
- error_code = test_data.get("error", {}).get("code", -1)
- logger.error(f"API в свежей сессии ОШИБКА: {error_code}")
- except Exception as e:
- logger.error(f"Ошибка при тестировании свежей сессии: {str(e)}")
-
- # Шаг 4: Получаем информацию об остальных API
- logger.info("Шаг 4: Получаем информацию о доступных API для уточнения проблемы")
-
- # Запрашиваем все API из SYNO.API.Info
- try:
- all_api_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "all"
- }
-
- all_api_response = session.get(api_info_url, params=all_api_params, timeout=15) # Больший таймаут для большого ответа
- all_api_data = all_api_response.json()
-
- if all_api_data.get("success"):
- api_list = all_api_data.get("data", {})
- logger.info(f"Получен список всех API. Найдено {len(api_list)} API.")
-
- # Ищем интересующие нас API для отладки
- interested_in = ["SYNO.DSM.Info", "SYNO.Core.System", "SYNO.Core.Hardware",
- "SYNO.Core.System.Status", "SYNO.API.Auth"]
-
- logger.info("Информация о важных API:")
- for api_name in interested_in:
- if api_name in api_list:
- logger.info(f"{api_name}: {api_list[api_name]}")
- else:
- logger.warning(f"API {api_name} не найден")
- else:
- logger.error("Не удалось получить список всех API")
- except Exception as e:
- logger.error(f"Ошибка при получении списка API: {str(e)}")
-
- else:
- error_code = auth_data.get("error", {}).get("code", -1)
- logger.error(f"Авторизация не удалась! Код ошибки: {error_code}")
- else:
- logger.error("Не удалось получить информацию об API авторизации")
-
- except Exception as e:
- logger.error(f"Произошла ошибка при выполнении теста: {str(e)}")
-
-if __name__ == "__main__":
- logger.info("Запуск прямого теста API Synology")
- direct_api_test()
diff --git a/.history/docker-compose_20250830102643.yml b/.history/docker-compose_20250830102643.yml
deleted file mode 100644
index a12f9b3..0000000
--- a/.history/docker-compose_20250830102643.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-version: '3.8'
-
-services:
- synology-bot:
- build:
- context: .
- dockerfile: Dockerfile
- container_name: synology-power-control-bot
- restart: unless-stopped
- env_file:
- - .env
- volumes:
- - ./logs:/app/logs
- # Если у вас есть файлы конфигурации или данные, которые нужно сохранять:
- # - ./data:/app/data
- networks:
- - bot-network
-
-networks:
- bot-network:
- driver: bridge
diff --git a/.history/docker-compose_20250830102820.yml b/.history/docker-compose_20250830102820.yml
deleted file mode 100644
index a74cbdf..0000000
--- a/.history/docker-compose_20250830102820.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-version: '3.8'
-
-services:
- synology-bot:
- build:
- context: .
- dockerfile: Dockerfile
- container_name: synology-power-control-bot
- restart: unless-stopped
- env_file:
- - .env
- volumes:
- - ./logs:/app/logs
- # Если у вас есть файлы конфигурации или данные, которые нужно сохранять:
- # - ./data:/app/data
- healthcheck:
- test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8080/health', timeout=5)"]
- interval: 60s
- timeout: 10s
- retries: 3
- start_period: 20s
- networks:
- - bot-network
- # Для ограничения ресурсов (раскомментируйте и настройте при необходимости):
- # deploy:
- # resources:
- # limits:
- # cpus: '0.50'
- # memory: 512M
- # reservations:
- # cpus: '0.25'
- # memory: 256M
-
-networks:
- bot-network:
- driver: bridge
diff --git a/.history/docker-compose_20250830102916.yml b/.history/docker-compose_20250830102916.yml
deleted file mode 100644
index c35284c..0000000
--- a/.history/docker-compose_20250830102916.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-version: '3.8'
-
-services:
- synology-bot:
- build:
- context: .
- dockerfile: Dockerfile
- container_name: synology-power-control-bot
- restart: unless-stopped
- env_file:
- - .env
- environment:
- - DOCKER_ENV=true
- volumes:
- - ./logs:/app/logs
- # Если у вас есть файлы конфигурации или данные, которые нужно сохранять:
- # - ./data:/app/data
- healthcheck:
- test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8080/health', timeout=5)"]
- interval: 60s
- timeout: 10s
- retries: 3
- start_period: 20s
- networks:
- - bot-network
- # Для ограничения ресурсов (раскомментируйте и настройте при необходимости):
- # deploy:
- # resources:
- # limits:
- # cpus: '0.50'
- # memory: 512M
- # reservations:
- # cpus: '0.25'
- # memory: 256M
-
-networks:
- bot-network:
- driver: bridge
diff --git a/.history/docker-compose_20250830103154.yml b/.history/docker-compose_20250830103154.yml
deleted file mode 100644
index c35284c..0000000
--- a/.history/docker-compose_20250830103154.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-version: '3.8'
-
-services:
- synology-bot:
- build:
- context: .
- dockerfile: Dockerfile
- container_name: synology-power-control-bot
- restart: unless-stopped
- env_file:
- - .env
- environment:
- - DOCKER_ENV=true
- volumes:
- - ./logs:/app/logs
- # Если у вас есть файлы конфигурации или данные, которые нужно сохранять:
- # - ./data:/app/data
- healthcheck:
- test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8080/health', timeout=5)"]
- interval: 60s
- timeout: 10s
- retries: 3
- start_period: 20s
- networks:
- - bot-network
- # Для ограничения ресурсов (раскомментируйте и настройте при необходимости):
- # deploy:
- # resources:
- # limits:
- # cpus: '0.50'
- # memory: 512M
- # reservations:
- # cpus: '0.25'
- # memory: 256M
-
-networks:
- bot-network:
- driver: bridge
diff --git a/.history/docs/file_manager_agent_20250830141933.md b/.history/docs/file_manager_agent_20250830141933.md
deleted file mode 100644
index fcd3892..0000000
--- a/.history/docs/file_manager_agent_20250830141933.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# Агент файлового менеджера для Synology Power Control Bot
-
-## Описание
-
-Агент файлового менеджера предоставляет удобный интерфейс для просмотра, управления и манипулирования файловой системой Synology NAS через Telegram бота. Агент разработан с использованием модульной архитектуры и предоставляет интуитивно понятный интерфейс с кнопками и диалоговыми окнами.
-
-## Функциональность
-
-- **Просмотр содержимого директорий** - навигация по файловой системе NAS
-- **Загрузка и скачивание файлов** - передача файлов между NAS и устройством пользователя
-- **Управление файлами** - переименование, удаление, получение информации о файлах
-- **Создание папок** - создание новых директорий на NAS
-- **Пагинация** - удобная навигация при большом количестве файлов
-
-## Использование
-
-Для начала работы с файловым менеджером отправьте команду `/files` боту. После этого вы увидите список доступных общих папок на вашем NAS. Используйте интерактивные кнопки для навигации и выполнения различных действий с файлами.
-
-### Основные команды
-
-- `/files` - запуск файлового менеджера
-- `/files [path]` - открытие файлового менеджера с указанным путем
-
-### Интерфейс и навигация
-
-Интерфейс файлового менеджера состоит из:
-- Информации о текущей директории (путь, количество файлов и папок)
-- Списка папок и файлов с кнопками для взаимодействия
-- Кнопок навигации (Вверх, Вперед, Назад)
-- Кнопок действий (Загрузить файл, Создать папку)
-
-## Структура кода
-
-Агент файлового менеджера состоит из следующих основных компонентов:
-
-- **FileManagerAgent** - основной класс агента, реализующий логику файлового менеджера
-- **SynologyAPI** - класс для взаимодействия с API Synology NAS
-- **filestation.py** - модуль, расширяющий SynologyAPI методами для работы с файлами
-
-## Интеграция
-
-Агент файлового менеджера можно легко интегрировать в любого Telegram бота с помощью функции `create_file_manager_handler()`, которая возвращает готовый `ConversationHandler` для регистрации в диспетчере бота.
-
-```python
-from src.api.synology import SynologyAPI
-from src.agents.file_manager_agent import create_file_manager_handler
-from src.api.filestation import add_file_manager_methods_to_synology_api
-
-# Создание экземпляра API
-synology_api = SynologyAPI()
-
-# Создание обработчика
-file_manager_handler = create_file_manager_handler(synology_api)
-
-# Регистрация обработчика в приложении бота
-application.add_handler(file_manager_handler)
-```
-
-## Безопасность
-
-Агент файлового менеджера использует декоратор `@admin_required` для обеспечения доступа только авторизованным пользователям. Это защищает файловую систему NAS от несанкционированного доступа.
diff --git a/.history/docs/file_manager_agent_20250830141957.md b/.history/docs/file_manager_agent_20250830141957.md
deleted file mode 100644
index fcd3892..0000000
--- a/.history/docs/file_manager_agent_20250830141957.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# Агент файлового менеджера для Synology Power Control Bot
-
-## Описание
-
-Агент файлового менеджера предоставляет удобный интерфейс для просмотра, управления и манипулирования файловой системой Synology NAS через Telegram бота. Агент разработан с использованием модульной архитектуры и предоставляет интуитивно понятный интерфейс с кнопками и диалоговыми окнами.
-
-## Функциональность
-
-- **Просмотр содержимого директорий** - навигация по файловой системе NAS
-- **Загрузка и скачивание файлов** - передача файлов между NAS и устройством пользователя
-- **Управление файлами** - переименование, удаление, получение информации о файлах
-- **Создание папок** - создание новых директорий на NAS
-- **Пагинация** - удобная навигация при большом количестве файлов
-
-## Использование
-
-Для начала работы с файловым менеджером отправьте команду `/files` боту. После этого вы увидите список доступных общих папок на вашем NAS. Используйте интерактивные кнопки для навигации и выполнения различных действий с файлами.
-
-### Основные команды
-
-- `/files` - запуск файлового менеджера
-- `/files [path]` - открытие файлового менеджера с указанным путем
-
-### Интерфейс и навигация
-
-Интерфейс файлового менеджера состоит из:
-- Информации о текущей директории (путь, количество файлов и папок)
-- Списка папок и файлов с кнопками для взаимодействия
-- Кнопок навигации (Вверх, Вперед, Назад)
-- Кнопок действий (Загрузить файл, Создать папку)
-
-## Структура кода
-
-Агент файлового менеджера состоит из следующих основных компонентов:
-
-- **FileManagerAgent** - основной класс агента, реализующий логику файлового менеджера
-- **SynologyAPI** - класс для взаимодействия с API Synology NAS
-- **filestation.py** - модуль, расширяющий SynologyAPI методами для работы с файлами
-
-## Интеграция
-
-Агент файлового менеджера можно легко интегрировать в любого Telegram бота с помощью функции `create_file_manager_handler()`, которая возвращает готовый `ConversationHandler` для регистрации в диспетчере бота.
-
-```python
-from src.api.synology import SynologyAPI
-from src.agents.file_manager_agent import create_file_manager_handler
-from src.api.filestation import add_file_manager_methods_to_synology_api
-
-# Создание экземпляра API
-synology_api = SynologyAPI()
-
-# Создание обработчика
-file_manager_handler = create_file_manager_handler(synology_api)
-
-# Регистрация обработчика в приложении бота
-application.add_handler(file_manager_handler)
-```
-
-## Безопасность
-
-Агент файлового менеджера использует декоратор `@admin_required` для обеспечения доступа только авторизованным пользователям. Это защищает файловую систему NAS от несанкционированного доступа.
diff --git a/.history/entrypoint_20250830102747.sh b/.history/entrypoint_20250830102747.sh
deleted file mode 100644
index b4de286..0000000
--- a/.history/entrypoint_20250830102747.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/sh
-
-# Создаем директорию для логов, если она не существует
-mkdir -p /app/logs
-
-# Запускаем бота
-exec python /app/run.py
diff --git a/.history/entrypoint_20250830103154.sh b/.history/entrypoint_20250830103154.sh
deleted file mode 100644
index b4de286..0000000
--- a/.history/entrypoint_20250830103154.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/sh
-
-# Создаем директорию для логов, если она не существует
-mkdir -p /app/logs
-
-# Запускаем бота
-exec python /app/run.py
diff --git a/.history/examples/file_manager_demo_20250830141907.py b/.history/examples/file_manager_demo_20250830141907.py
deleted file mode 100644
index 66bb463..0000000
--- a/.history/examples/file_manager_demo_20250830141907.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Пример использования файлового менеджера для Synology NAS
-"""
-
-import logging
-import asyncio
-from telegram.ext import Application
-
-from src.config.config import TELEGRAM_TOKEN
-from src.api.synology import SynologyAPI
-from src.agents.file_manager_agent import create_file_manager_handler
-from src.api.filestation import add_file_manager_methods_to_synology_api
-from src.utils.logger import setup_logging
-
-async def main():
- """Главная функция демонстрации файлового менеджера"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology File Manager Demo")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Создание экземпляра API и добавление методов для работы с файловой системой
- synology_api = SynologyAPI()
-
- # Регистрация обработчика файлового менеджера
- file_manager_handler = create_file_manager_handler(synology_api)
- application.add_handler(file_manager_handler)
-
- # Запуск бота
- logger.info("Bot started with file manager. Use /files command to start. Press Ctrl+C to stop.")
- await application.start()
- await application.updater.start_polling()
-
- # Ждем прерывание
- try:
- await asyncio.Future() # Бесконечное ожидание
- finally:
- await application.stop()
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/.history/examples/file_manager_demo_20250830141957.py b/.history/examples/file_manager_demo_20250830141957.py
deleted file mode 100644
index 66bb463..0000000
--- a/.history/examples/file_manager_demo_20250830141957.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Пример использования файлового менеджера для Synology NAS
-"""
-
-import logging
-import asyncio
-from telegram.ext import Application
-
-from src.config.config import TELEGRAM_TOKEN
-from src.api.synology import SynologyAPI
-from src.agents.file_manager_agent import create_file_manager_handler
-from src.api.filestation import add_file_manager_methods_to_synology_api
-from src.utils.logger import setup_logging
-
-async def main():
- """Главная функция демонстрации файлового менеджера"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology File Manager Demo")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Создание экземпляра API и добавление методов для работы с файловой системой
- synology_api = SynologyAPI()
-
- # Регистрация обработчика файлового менеджера
- file_manager_handler = create_file_manager_handler(synology_api)
- application.add_handler(file_manager_handler)
-
- # Запуск бота
- logger.info("Bot started with file manager. Use /files command to start. Press Ctrl+C to stop.")
- await application.start()
- await application.updater.start_polling()
-
- # Ждем прерывание
- try:
- await asyncio.Future() # Бесконечное ожидание
- finally:
- await application.stop()
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/.history/requirements_20250830063740.txt b/.history/requirements_20250830063740.txt
deleted file mode 100644
index d08038f..0000000
--- a/.history/requirements_20250830063740.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-python-telegram-bot>=20.0
-requests>=2.28.0
-python-dotenv>=1.0.0
-urllib3>=2.0.0
diff --git a/.history/requirements_20250830063839.txt b/.history/requirements_20250830063839.txt
deleted file mode 100644
index d08038f..0000000
--- a/.history/requirements_20250830063839.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-python-telegram-bot>=20.0
-requests>=2.28.0
-python-dotenv>=1.0.0
-urllib3>=2.0.0
diff --git a/.history/requirements_20250830065002.txt b/.history/requirements_20250830065002.txt
deleted file mode 100644
index 155425c..0000000
--- a/.history/requirements_20250830065002.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-python-telegram-bot>=20.0
-requests>=2.28.0
-python-dotenv>=1.0.0
-urllib3>=2.0.0
-python-synology>=0.4.0
diff --git a/.history/requirements_20250830065455.txt b/.history/requirements_20250830065455.txt
deleted file mode 100644
index 155425c..0000000
--- a/.history/requirements_20250830065455.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-python-telegram-bot>=20.0
-requests>=2.28.0
-python-dotenv>=1.0.0
-urllib3>=2.0.0
-python-synology>=0.4.0
diff --git a/.history/requirements_20250830072350.txt b/.history/requirements_20250830072350.txt
deleted file mode 100644
index d08038f..0000000
--- a/.history/requirements_20250830072350.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-python-telegram-bot>=20.0
-requests>=2.28.0
-python-dotenv>=1.0.0
-urllib3>=2.0.0
diff --git a/.history/requirements_20250830072817.txt b/.history/requirements_20250830072817.txt
deleted file mode 100644
index d08038f..0000000
--- a/.history/requirements_20250830072817.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-python-telegram-bot>=20.0
-requests>=2.28.0
-python-dotenv>=1.0.0
-urllib3>=2.0.0
diff --git a/.history/requirements_20250830092418.txt b/.history/requirements_20250830092418.txt
deleted file mode 100644
index 4703994..0000000
--- a/.history/requirements_20250830092418.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-python-telegram-bot>=20.0
-requests>=2.28.0
-python-dotenv>=1.0.0
-urllib3>=2.0.0
-aiohttp>=3.8.4
-async-timeout>=4.0.2
diff --git a/.history/requirements_20250830092441.txt b/.history/requirements_20250830092441.txt
deleted file mode 100644
index 4703994..0000000
--- a/.history/requirements_20250830092441.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-python-telegram-bot>=20.0
-requests>=2.28.0
-python-dotenv>=1.0.0
-urllib3>=2.0.0
-aiohttp>=3.8.4
-async-timeout>=4.0.2
diff --git a/.history/run_20250830063754.py b/.history/run_20250830063754.py
deleted file mode 100644
index bf8b3e5..0000000
--- a/.history/run_20250830063754.py
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Точка входа для запуска телеграм-бота
-"""
-
-from src.bot import main
-
-if __name__ == "__main__":
- main()
diff --git a/.history/run_20250830063839.py b/.history/run_20250830063839.py
deleted file mode 100644
index bf8b3e5..0000000
--- a/.history/run_20250830063839.py
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Точка входа для запуска телеграм-бота
-"""
-
-from src.bot import main
-
-if __name__ == "__main__":
- main()
diff --git a/.history/run_20250830102904.py b/.history/run_20250830102904.py
deleted file mode 100644
index b695a8d..0000000
--- a/.history/run_20250830102904.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Точка входа для запуска телеграм-бота
-"""
-
-import os
-from src.bot import main
-from src.healthcheck import start_health_server
-
-if __name__ == "__main__":
- # Запускаем healthcheck сервер в Docker-окружении
- if os.environ.get("DOCKER_ENV", "False").lower() == "true":
- start_health_server()
-
- # Запускаем основной бот
- main()
diff --git a/.history/run_20250830103154.py b/.history/run_20250830103154.py
deleted file mode 100644
index b695a8d..0000000
--- a/.history/run_20250830103154.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Точка входа для запуска телеграм-бота
-"""
-
-import os
-from src.bot import main
-from src.healthcheck import start_health_server
-
-if __name__ == "__main__":
- # Запускаем healthcheck сервер в Docker-окружении
- if os.environ.get("DOCKER_ENV", "False").lower() == "true":
- start_health_server()
-
- # Запускаем основной бот
- main()
diff --git a/.history/run_bot_20250830082521.py b/.history/run_bot_20250830082521.py
deleted file mode 100644
index 737f58d..0000000
--- a/.history/run_bot_20250830082521.py
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Файл-обёртка для запуска бота из корневой директории
-"""
-
-from src.bot import main
-
-if __name__ == "__main__":
- main()
diff --git a/.history/run_bot_20250830082536.py b/.history/run_bot_20250830082536.py
deleted file mode 100644
index 737f58d..0000000
--- a/.history/run_bot_20250830082536.py
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Файл-обёртка для запуска бота из корневой директории
-"""
-
-from src.bot import main
-
-if __name__ == "__main__":
- main()
diff --git a/.history/run_bot_20250830142127.py b/.history/run_bot_20250830142127.py
deleted file mode 100644
index edcba94..0000000
--- a/.history/run_bot_20250830142127.py
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Файл-обёртка для запуска бота из корневой директории
-"""
-
-from src.bot import main
-
-if __name__ == "__main__":
- main()
-
-& C:/Users/sst/synology_power_control_bot/.venv/Scripts/python.exe c:/Users/sst/synology_power_control_bot/run_bot.py
diff --git a/.history/run_bot_20250830142131.py b/.history/run_bot_20250830142131.py
deleted file mode 100644
index 737f58d..0000000
--- a/.history/run_bot_20250830142131.py
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Файл-обёртка для запуска бота из корневой директории
-"""
-
-from src.bot import main
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/agents/__init___20250830141428.py b/.history/src/agents/__init___20250830141428.py
deleted file mode 100644
index ce1aa94..0000000
--- a/.history/src/agents/__init___20250830141428.py
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль агентов для Synology Power Control Bot.
-Содержит функциональные агенты, реализующие различные возможности бота.
-"""
-
-from src.agents.file_manager_agent import FileManagerAgent, create_file_manager_handler
diff --git a/.history/src/agents/__init___20250830141957.py b/.history/src/agents/__init___20250830141957.py
deleted file mode 100644
index ce1aa94..0000000
--- a/.history/src/agents/__init___20250830141957.py
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль агентов для Synology Power Control Bot.
-Содержит функциональные агенты, реализующие различные возможности бота.
-"""
-
-from src.agents.file_manager_agent import FileManagerAgent, create_file_manager_handler
diff --git a/.history/src/agents/file_manager_agent_20250830141230.py b/.history/src/agents/file_manager_agent_20250830141230.py
deleted file mode 100644
index 84aebc3..0000000
--- a/.history/src/agents/file_manager_agent_20250830141230.py
+++ /dev/null
@@ -1,653 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- ParseMode,
- InputFile
-)
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id
-
- if ":prev:" in callback_data:
- # Предыдущая страница
- path = callback_data.split("fm:nav:prev:")[1]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif ":next:" in callback_data:
- # Следующая страница
- path = callback_data.split("fm:nav:next:")[1]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif ":refresh:" in callback_data:
- # Обновить текущую директорию
- path = callback_data.split("fm:nav:refresh:")[1]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif "fm:nav:close" in callback_data:
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- while size_bytes >= 1024 and i < len(size_names) - 1:
- size_bytes /= 1024.0
- i += 1
-
- return f"{size_bytes:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- # Здесь будет обработчик для получения нового имени файла
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- # Здесь будет обработчик для получения имени новой папки
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", lambda u, c: ConversationHandler.END),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830141546.py b/.history/src/agents/file_manager_agent_20250830141546.py
deleted file mode 100644
index 38ecf1d..0000000
--- a/.history/src/agents/file_manager_agent_20250830141546.py
+++ /dev/null
@@ -1,653 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- ParseMode,
- InputFile
-)
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id
-
- if ":prev:" in callback_data:
- # Предыдущая страница
- path = callback_data.split("fm:nav:prev:")[1]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif ":next:" in callback_data:
- # Следующая страница
- path = callback_data.split("fm:nav:next:")[1]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif ":refresh:" in callback_data:
- # Обновить текущую директорию
- path = callback_data.split("fm:nav:refresh:")[1]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif "fm:nav:close" in callback_data:
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- while size_bytes >= 1024 and i < len(size_names) - 1:
- size_bytes /= 1024.0
- i += 1
-
- return f"{size_bytes:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", lambda u, c: ConversationHandler.END),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830141613.py b/.history/src/agents/file_manager_agent_20250830141613.py
deleted file mode 100644
index d843a23..0000000
--- a/.history/src/agents/file_manager_agent_20250830141613.py
+++ /dev/null
@@ -1,693 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- ParseMode,
- InputFile
-)
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id
-
- if ":prev:" in callback_data:
- # Предыдущая страница
- path = callback_data.split("fm:nav:prev:")[1]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif ":next:" in callback_data:
- # Следующая страница
- path = callback_data.split("fm:nav:next:")[1]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif ":refresh:" in callback_data:
- # Обновить текущую директорию
- path = callback_data.split("fm:nav:refresh:")[1]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif "fm:nav:close" in callback_data:
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- while size_bytes >= 1024 and i < len(size_names) - 1:
- size_bytes /= 1024.0
- i += 1
-
- return f"{size_bytes:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", lambda u, c: ConversationHandler.END),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830141646.py b/.history/src/agents/file_manager_agent_20250830141646.py
deleted file mode 100644
index 6eca0da..0000000
--- a/.history/src/agents/file_manager_agent_20250830141646.py
+++ /dev/null
@@ -1,743 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- ParseMode,
- InputFile
-)
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id
-
- if ":prev:" in callback_data:
- # Предыдущая страница
- path = callback_data.split("fm:nav:prev:")[1]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif ":next:" in callback_data:
- # Следующая страница
- path = callback_data.split("fm:nav:next:")[1]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif ":refresh:" in callback_data:
- # Обновить текущую директорию
- path = callback_data.split("fm:nav:refresh:")[1]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif "fm:nav:close" in callback_data:
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- while size_bytes >= 1024 and i < len(size_names) - 1:
- size_bytes /= 1024.0
- i += 1
-
- return f"{size_bytes:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", lambda u, c: ConversationHandler.END),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830141721.py b/.history/src/agents/file_manager_agent_20250830141721.py
deleted file mode 100644
index e62fac1..0000000
--- a/.history/src/agents/file_manager_agent_20250830141721.py
+++ /dev/null
@@ -1,750 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- ParseMode,
- InputFile
-)
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id
-
- if ":prev:" in callback_data:
- # Предыдущая страница
- path = callback_data.split("fm:nav:prev:")[1]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif ":next:" in callback_data:
- # Следующая страница
- path = callback_data.split("fm:nav:next:")[1]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif ":refresh:" in callback_data:
- # Обновить текущую директорию
- path = callback_data.split("fm:nav:refresh:")[1]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif "fm:nav:close" in callback_data:
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- while size_bytes >= 1024 and i < len(size_names) - 1:
- size_bytes /= 1024.0
- i += 1
-
- return f"{size_bytes:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", lambda u, c: ConversationHandler.END),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830141747.py b/.history/src/agents/file_manager_agent_20250830141747.py
deleted file mode 100644
index d3d4e57..0000000
--- a/.history/src/agents/file_manager_agent_20250830141747.py
+++ /dev/null
@@ -1,750 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- ParseMode,
- InputFile
-)
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- while size_bytes >= 1024 and i < len(size_names) - 1:
- size_bytes /= 1024.0
- i += 1
-
- return f"{size_bytes:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", lambda u, c: ConversationHandler.END),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830141805.py b/.history/src/agents/file_manager_agent_20250830141805.py
deleted file mode 100644
index 53f9a03..0000000
--- a/.history/src/agents/file_manager_agent_20250830141805.py
+++ /dev/null
@@ -1,751 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- ParseMode,
- InputFile
-)
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", lambda u, c: ConversationHandler.END),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830141832.py b/.history/src/agents/file_manager_agent_20250830141832.py
deleted file mode 100644
index 66c46f6..0000000
--- a/.history/src/agents/file_manager_agent_20250830141832.py
+++ /dev/null
@@ -1,756 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- ParseMode,
- InputFile
-)
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830141847.py b/.history/src/agents/file_manager_agent_20250830141847.py
deleted file mode 100644
index ff5c408..0000000
--- a/.history/src/agents/file_manager_agent_20250830141847.py
+++ /dev/null
@@ -1,757 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- ParseMode,
- InputFile
-)
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830141957.py b/.history/src/agents/file_manager_agent_20250830141957.py
deleted file mode 100644
index ff5c408..0000000
--- a/.history/src/agents/file_manager_agent_20250830141957.py
+++ /dev/null
@@ -1,757 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- ParseMode,
- InputFile
-)
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830142055.py b/.history/src/agents/file_manager_agent_20250830142055.py
deleted file mode 100644
index 107dca2..0000000
--- a/.history/src/agents/file_manager_agent_20250830142055.py
+++ /dev/null
@@ -1,757 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830142117.py b/.history/src/agents/file_manager_agent_20250830142117.py
deleted file mode 100644
index 107dca2..0000000
--- a/.history/src/agents/file_manager_agent_20250830142117.py
+++ /dev/null
@@ -1,757 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830142754.py b/.history/src/agents/file_manager_agent_20250830142754.py
deleted file mode 100644
index 125e9a9..0000000
--- a/.history/src/agents/file_manager_agent_20250830142754.py
+++ /dev/null
@@ -1,760 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830142812.py b/.history/src/agents/file_manager_agent_20250830142812.py
deleted file mode 100644
index edf5b15..0000000
--- a/.history/src/agents/file_manager_agent_20250830142812.py
+++ /dev/null
@@ -1,763 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830142848.py b/.history/src/agents/file_manager_agent_20250830142848.py
deleted file mode 100644
index b9ddd27..0000000
--- a/.history/src/agents/file_manager_agent_20250830142848.py
+++ /dev/null
@@ -1,766 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830142901.py b/.history/src/agents/file_manager_agent_20250830142901.py
deleted file mode 100644
index 748a2de..0000000
--- a/.history/src/agents/file_manager_agent_20250830142901.py
+++ /dev/null
@@ -1,769 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие файла
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830142941.py b/.history/src/agents/file_manager_agent_20250830142941.py
deleted file mode 100644
index c6a7314..0000000
--- a/.history/src/agents/file_manager_agent_20250830142941.py
+++ /dev/null
@@ -1,775 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830143005.py b/.history/src/agents/file_manager_agent_20250830143005.py
deleted file mode 100644
index bc826d6..0000000
--- a/.history/src/agents/file_manager_agent_20250830143005.py
+++ /dev/null
@@ -1,775 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830143049.py b/.history/src/agents/file_manager_agent_20250830143049.py
deleted file mode 100644
index 9c8a057..0000000
--- a/.history/src/agents/file_manager_agent_20250830143049.py
+++ /dev/null
@@ -1,775 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830143114.py b/.history/src/agents/file_manager_agent_20250830143114.py
deleted file mode 100644
index 2a89c4c..0000000
--- a/.history/src/agents/file_manager_agent_20250830143114.py
+++ /dev/null
@@ -1,785 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not update.message:
- return BROWSING
-
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите текст для имени файла."
- )
- return RENAMING
-
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830143155.py b/.history/src/agents/file_manager_agent_20250830143155.py
deleted file mode 100644
index 2a89c4c..0000000
--- a/.history/src/agents/file_manager_agent_20250830143155.py
+++ /dev/null
@@ -1,785 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not update.message:
- return BROWSING
-
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите текст для имени файла."
- )
- return RENAMING
-
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830143317.py b/.history/src/agents/file_manager_agent_20250830143317.py
deleted file mode 100644
index 90dfea3..0000000
--- a/.history/src/agents/file_manager_agent_20250830143317.py
+++ /dev/null
@@ -1,784 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not update.message:
- return BROWSING
-
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите текст для имени файла."
- )
- return RENAMING
-
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- # context.user_data - это уже существующий словарь, просто добавляем в него данные
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830143333.py b/.history/src/agents/file_manager_agent_20250830143333.py
deleted file mode 100644
index d1906ea..0000000
--- a/.history/src/agents/file_manager_agent_20250830143333.py
+++ /dev/null
@@ -1,789 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not update.message:
- return BROWSING
-
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите текст для имени файла."
- )
- return RENAMING
-
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # Сохраняем информацию о создании папки в контексте пользователя
- # context.user_data может быть инициализирован как None
- if context.user_data is None:
- # В таком случае инициализируем его как dict через контекст
- context.chat_data.clear() # Этот трюк инициализирует user_data
-
- # Теперь безопасно используем user_data
- context.user_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830143351.py b/.history/src/agents/file_manager_agent_20250830143351.py
deleted file mode 100644
index 69ada98..0000000
--- a/.history/src/agents/file_manager_agent_20250830143351.py
+++ /dev/null
@@ -1,794 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not update.message:
- return BROWSING
-
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите текст для имени файла."
- )
- return RENAMING
-
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # В PTB 20+ user_data должен быть всегда доступен
- # Просто добавляем нашу информацию в словарь
- # Если context.user_data не инициализирован, используем setdefault
- # чтобы добавить ключ, если его нет
- if hasattr(context, 'user_data') and context.user_data is not None:
- context.user_data['creating_folder'] = {
- 'path': path
- }
- else:
- # Если по какой-то причине user_data недоступен,
- # запишем путь в context.chat_data (он более стабилен)
- if hasattr(context, 'chat_data') and context.chat_data is not None:
- context.chat_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- if not context.user_data or not context.user_data.get('creating_folder'):
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- parent_path = context.user_data['creating_folder'].get('path')
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830143422.py b/.history/src/agents/file_manager_agent_20250830143422.py
deleted file mode 100644
index e5df44c..0000000
--- a/.history/src/agents/file_manager_agent_20250830143422.py
+++ /dev/null
@@ -1,812 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if not context.user_data:
- context.user_data = {}
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not update.message:
- return BROWSING
-
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите текст для имени файла."
- )
- return RENAMING
-
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # В PTB 20+ user_data должен быть всегда доступен
- # Просто добавляем нашу информацию в словарь
- # Если context.user_data не инициализирован, используем setdefault
- # чтобы добавить ключ, если его нет
- if hasattr(context, 'user_data') and context.user_data is not None:
- context.user_data['creating_folder'] = {
- 'path': path
- }
- else:
- # Если по какой-то причине user_data недоступен,
- # запишем путь в context.chat_data (он более стабилен)
- if hasattr(context, 'chat_data') and context.chat_data is not None:
- context.chat_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- # Проверяем где может быть информация о папке - в user_data или в chat_data
- parent_path = None
-
- # Сначала проверяем user_data
- if hasattr(context, 'user_data') and context.user_data is not None:
- if 'creating_folder' in context.user_data:
- parent_path = context.user_data['creating_folder'].get('path')
-
- # Если не нашли в user_data, проверяем в chat_data
- if parent_path is None and hasattr(context, 'chat_data') and context.chat_data is not None:
- if 'creating_folder' in context.chat_data:
- parent_path = context.chat_data['creating_folder'].get('path')
-
- if parent_path is None:
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя."
- )
- return CREATING_FOLDER
-
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830143501.py b/.history/src/agents/file_manager_agent_20250830143501.py
deleted file mode 100644
index fae0a91..0000000
--- a/.history/src/agents/file_manager_agent_20250830143501.py
+++ /dev/null
@@ -1,817 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if hasattr(context, 'user_data') and context.user_data is not None:
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
- # Дополнительно сохраняем в chat_data для надежности
- if hasattr(context, 'chat_data') and context.chat_data is not None:
- context.chat_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not update.message:
- return BROWSING
-
- if not context.user_data or 'renaming' not in context.user_data:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
-
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
- old_name = os.path.basename(file_path)
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите текст для имени файла."
- )
- return RENAMING
-
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # В PTB 20+ user_data должен быть всегда доступен
- # Просто добавляем нашу информацию в словарь
- # Если context.user_data не инициализирован, используем setdefault
- # чтобы добавить ключ, если его нет
- if hasattr(context, 'user_data') and context.user_data is not None:
- context.user_data['creating_folder'] = {
- 'path': path
- }
- else:
- # Если по какой-то причине user_data недоступен,
- # запишем путь в context.chat_data (он более стабилен)
- if hasattr(context, 'chat_data') and context.chat_data is not None:
- context.chat_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- # Проверяем где может быть информация о папке - в user_data или в chat_data
- parent_path = None
-
- # Сначала проверяем user_data
- if hasattr(context, 'user_data') and context.user_data is not None:
- if 'creating_folder' in context.user_data:
- parent_path = context.user_data['creating_folder'].get('path')
-
- # Если не нашли в user_data, проверяем в chat_data
- if parent_path is None and hasattr(context, 'chat_data') and context.chat_data is not None:
- if 'creating_folder' in context.chat_data:
- parent_path = context.chat_data['creating_folder'].get('path')
-
- if parent_path is None:
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя."
- )
- return CREATING_FOLDER
-
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830143531.py b/.history/src/agents/file_manager_agent_20250830143531.py
deleted file mode 100644
index 4d9b23c..0000000
--- a/.history/src/agents/file_manager_agent_20250830143531.py
+++ /dev/null
@@ -1,828 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if hasattr(context, 'user_data') and context.user_data is not None:
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
- # Дополнительно сохраняем в chat_data для надежности
- if hasattr(context, 'chat_data') and context.chat_data is not None:
- context.chat_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not update.message:
- return BROWSING
-
- # Проверяем где может быть информация о файле - в user_data или в chat_data
- file_path = None
- file_dir = None
-
- # Сначала проверяем user_data
- if hasattr(context, 'user_data') and context.user_data is not None and 'renaming' in context.user_data:
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
-
- # Если не нашли в user_data, проверяем в chat_data
- if (file_path is None or file_dir is None) and hasattr(context, 'chat_data') and context.chat_data is not None and 'renaming' in context.chat_data:
- file_path = context.chat_data['renaming'].get('file_path')
- file_dir = context.chat_data['renaming'].get('file_dir')
-
- if file_path is None or file_dir is None:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
- old_name = os.path.basename(file_path)
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите текст для имени файла."
- )
- return RENAMING
-
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # В PTB 20+ user_data должен быть всегда доступен
- # Просто добавляем нашу информацию в словарь
- # Если context.user_data не инициализирован, используем setdefault
- # чтобы добавить ключ, если его нет
- if hasattr(context, 'user_data') and context.user_data is not None:
- context.user_data['creating_folder'] = {
- 'path': path
- }
- else:
- # Если по какой-то причине user_data недоступен,
- # запишем путь в context.chat_data (он более стабилен)
- if hasattr(context, 'chat_data') and context.chat_data is not None:
- context.chat_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- # Проверяем где может быть информация о папке - в user_data или в chat_data
- parent_path = None
-
- # Сначала проверяем user_data
- if hasattr(context, 'user_data') and context.user_data is not None:
- if 'creating_folder' in context.user_data:
- parent_path = context.user_data['creating_folder'].get('path')
-
- # Если не нашли в user_data, проверяем в chat_data
- if parent_path is None and hasattr(context, 'chat_data') and context.chat_data is not None:
- if 'creating_folder' in context.chat_data:
- parent_path = context.chat_data['creating_folder'].get('path')
-
- if parent_path is None:
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя."
- )
- return CREATING_FOLDER
-
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830143559.py b/.history/src/agents/file_manager_agent_20250830143559.py
deleted file mode 100644
index a39d2dd..0000000
--- a/.history/src/agents/file_manager_agent_20250830143559.py
+++ /dev/null
@@ -1,834 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if hasattr(context, 'user_data') and context.user_data is not None:
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
- # Дополнительно сохраняем в chat_data для надежности
- if hasattr(context, 'chat_data') and context.chat_data is not None:
- context.chat_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not update.message:
- return BROWSING
-
- # Проверяем где может быть информация о файле - в user_data или в chat_data
- file_path = None
- file_dir = None
-
- # Сначала проверяем user_data
- if hasattr(context, 'user_data') and context.user_data is not None and 'renaming' in context.user_data:
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
-
- # Если не нашли в user_data, проверяем в chat_data
- if (file_path is None or file_dir is None) and hasattr(context, 'chat_data') and context.chat_data is not None and 'renaming' in context.chat_data:
- file_path = context.chat_data['renaming'].get('file_path')
- file_dir = context.chat_data['renaming'].get('file_dir')
-
- if file_path is None or file_dir is None:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
- old_name = os.path.basename(file_path)
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите текст для имени файла."
- )
- return RENAMING
-
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if hasattr(context, 'user_data') and context.user_data is not None and 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- if hasattr(context, 'chat_data') and context.chat_data is not None and 'renaming' in context.chat_data:
- del context.chat_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # В PTB 20+ user_data должен быть всегда доступен
- # Просто добавляем нашу информацию в словарь
- # Если context.user_data не инициализирован, используем setdefault
- # чтобы добавить ключ, если его нет
- if hasattr(context, 'user_data') and context.user_data is not None:
- context.user_data['creating_folder'] = {
- 'path': path
- }
- else:
- # Если по какой-то причине user_data недоступен,
- # запишем путь в context.chat_data (он более стабилен)
- if hasattr(context, 'chat_data') and context.chat_data is not None:
- context.chat_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- # Проверяем где может быть информация о папке - в user_data или в chat_data
- parent_path = None
-
- # Сначала проверяем user_data
- if hasattr(context, 'user_data') and context.user_data is not None:
- if 'creating_folder' in context.user_data:
- parent_path = context.user_data['creating_folder'].get('path')
-
- # Если не нашли в user_data, проверяем в chat_data
- if parent_path is None and hasattr(context, 'chat_data') and context.chat_data is not None:
- if 'creating_folder' in context.chat_data:
- parent_path = context.chat_data['creating_folder'].get('path')
-
- if parent_path is None:
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя."
- )
- return CREATING_FOLDER
-
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Отображаем обновленное содержимое директории
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830143628.py b/.history/src/agents/file_manager_agent_20250830143628.py
deleted file mode 100644
index 7d20e9b..0000000
--- a/.history/src/agents/file_manager_agent_20250830143628.py
+++ /dev/null
@@ -1,844 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if hasattr(context, 'user_data') and context.user_data is not None:
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
- # Дополнительно сохраняем в chat_data для надежности
- if hasattr(context, 'chat_data') and context.chat_data is not None:
- context.chat_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not update.message:
- return BROWSING
-
- # Проверяем где может быть информация о файле - в user_data или в chat_data
- file_path = None
- file_dir = None
-
- # Сначала проверяем user_data
- if hasattr(context, 'user_data') and context.user_data is not None and 'renaming' in context.user_data:
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
-
- # Если не нашли в user_data, проверяем в chat_data
- if (file_path is None or file_dir is None) and hasattr(context, 'chat_data') and context.chat_data is not None and 'renaming' in context.chat_data:
- file_path = context.chat_data['renaming'].get('file_path')
- file_dir = context.chat_data['renaming'].get('file_dir')
-
- if file_path is None or file_dir is None:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
- old_name = os.path.basename(file_path)
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите текст для имени файла."
- )
- return RENAMING
-
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if hasattr(context, 'user_data') and context.user_data is not None and 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- if hasattr(context, 'chat_data') and context.chat_data is not None and 'renaming' in context.chat_data:
- del context.chat_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # В PTB 20+ user_data должен быть всегда доступен
- # Просто добавляем нашу информацию в словарь
- # Если context.user_data не инициализирован, используем setdefault
- # чтобы добавить ключ, если его нет
- if hasattr(context, 'user_data') and context.user_data is not None:
- context.user_data['creating_folder'] = {
- 'path': path
- }
- else:
- # Если по какой-то причине user_data недоступен,
- # запишем путь в context.chat_data (он более стабилен)
- if hasattr(context, 'chat_data') and context.chat_data is not None:
- context.chat_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- # Проверяем где может быть информация о папке - в user_data или в chat_data
- parent_path = None
-
- # Сначала проверяем user_data
- if hasattr(context, 'user_data') and context.user_data is not None:
- if 'creating_folder' in context.user_data:
- parent_path = context.user_data['creating_folder'].get('path')
-
- # Если не нашли в user_data, проверяем в chat_data
- if parent_path is None and hasattr(context, 'chat_data') and context.chat_data is not None:
- if 'creating_folder' in context.chat_data:
- parent_path = context.chat_data['creating_folder'].get('path')
-
- if parent_path is None:
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя."
- )
- return CREATING_FOLDER
-
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Очищаем данные о создании папки
- if hasattr(context, 'user_data') and context.user_data is not None and 'creating_folder' in context.user_data:
- del context.user_data['creating_folder']
-
- if hasattr(context, 'chat_data') and context.chat_data is not None and 'creating_folder' in context.chat_data:
- del context.chat_data['creating_folder']
-
- # Отображаем обновленное содержимое директории
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/agents/file_manager_agent_20250830143646.py b/.history/src/agents/file_manager_agent_20250830143646.py
deleted file mode 100644
index 7d20e9b..0000000
--- a/.history/src/agents/file_manager_agent_20250830143646.py
+++ /dev/null
@@ -1,844 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Агент файлового менеджера для Synology Power Control Bot.
-Предоставляет расширенный интерфейс для работы с файловой системой Synology NAS.
-"""
-
-import os
-import time
-import logging
-import html
-from typing import Dict, List, Any, Optional, Union, Tuple
-
-from telegram import (
- Update,
- InlineKeyboardButton,
- InlineKeyboardMarkup,
- InputFile
-)
-from telegram.constants import ParseMode
-from telegram.ext import (
- ContextTypes,
- ConversationHandler,
- CallbackQueryHandler,
- CommandHandler,
- MessageHandler,
- filters
-)
-
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-# Состояния для ConversationHandler
-BROWSING, UPLOADING, RENAMING, DELETING, CREATING_FOLDER = range(5)
-
-# Константы для максимального количества элементов на странице
-MAX_ITEMS_PER_PAGE = 10
-
-class FileManagerAgent:
- """Агент файлового менеджера для взаимодействия с файловой системой NAS."""
-
- def __init__(self, synology_api: SynologyAPI):
- """Инициализация агента файлового менеджера."""
- self.synology_api = synology_api
- self.user_data = {} # Хранилище для пользовательских данных (текущий путь и т.д.)
-
- # Создаем обработчики для регистрации в боте
- self.handlers = [
- CommandHandler("files", self.start_file_manager),
- CallbackQueryHandler(self.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(self.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(self.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(self.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(self.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(self.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(self.navigation_callback, pattern="^fm:nav:"),
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, self.handle_file_upload),
- ]
-
- def get_user_path(self, user_id: int) -> str:
- """Получает текущий путь для пользователя."""
- return self.user_data.get(user_id, {}).get('current_path', '/')
-
- def set_user_path(self, user_id: int, path: str) -> None:
- """Устанавливает текущий путь для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- self.user_data[user_id]['current_path'] = path
-
- def get_user_pagination(self, user_id: int) -> dict:
- """Получает информацию о пагинации для пользователя."""
- return self.user_data.get(user_id, {}).get('pagination', {'page': 0, 'total_pages': 1})
-
- def set_user_pagination(self, user_id: int, page: int, total_pages: int) -> None:
- """Устанавливает информацию о пагинации для пользователя."""
- if user_id not in self.user_data:
- self.user_data[user_id] = {}
- if 'pagination' not in self.user_data[user_id]:
- self.user_data[user_id]['pagination'] = {}
- self.user_data[user_id]['pagination']['page'] = page
- self.user_data[user_id]['pagination']['total_pages'] = total_pages
-
- @admin_required
- async def start_file_manager(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Запускает файловый менеджер."""
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
-
- # Устанавливаем начальный путь
- initial_path = '/'
- if context.args and context.args[0]:
- initial_path = context.args[0]
- self.set_user_path(user_id, initial_path)
-
- # Отображаем содержимое начального пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def display_directory_content(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Отображает содержимое директории."""
- if not update.effective_user:
- return
-
- user_id = update.effective_user.id
- current_path = self.get_user_path(user_id)
- pagination = self.get_user_pagination(user_id)
- current_page = pagination['page']
-
- # Получаем список файлов и папок
- files_and_folders = self.synology_api.list_files(current_path)
-
- if not files_and_folders:
- await self.send_or_edit_message(
- update,
- f"📁 Путь: {html.escape(current_path)}\n\n"
- f"📭 Папка пуста или недоступна",
- self.get_empty_folder_keyboard(current_path)
- )
- return
-
- # Разделяем на папки и файлы, сортируем по имени
- folders = sorted([item for item in files_and_folders if item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
- files = sorted([item for item in files_and_folders if not item.get('isdir', False)],
- key=lambda x: x.get('name', '').lower())
-
- # Подготавливаем информацию для пагинации
- all_items = folders + files
- total_items = len(all_items)
- total_pages = max(1, (total_items + MAX_ITEMS_PER_PAGE - 1) // MAX_ITEMS_PER_PAGE)
-
- # Корректируем текущую страницу, если она некорректна
- if current_page >= total_pages:
- current_page = 0
- elif current_page < 0:
- current_page = total_pages - 1
-
- # Обновляем информацию о пагинации
- self.set_user_pagination(user_id, current_page, total_pages)
-
- # Определяем диапазон элементов для текущей страницы
- start_idx = current_page * MAX_ITEMS_PER_PAGE
- end_idx = min(start_idx + MAX_ITEMS_PER_PAGE, total_items)
- current_items = all_items[start_idx:end_idx]
-
- # Формируем сообщение с информацией о директории
- message_text = f"📁 Путь: {html.escape(current_path)}\n\n"
- message_text += f"📂 Папок: {len(folders)}\n"
- message_text += f"📄 Файлов: {len(files)}\n"
-
- if files:
- total_size = sum(file.get('size', 0) for file in files)
- message_text += f"💾 Общий размер: {self.get_human_readable_size(total_size)}\n"
-
- message_text += f"\nСтраница {current_page + 1}/{total_pages}"
-
- # Формируем клавиатуру с элементами и навигационными кнопками
- keyboard = self.create_file_browser_keyboard(current_items, current_path, current_page, total_pages)
-
- # Отправляем или обновляем сообщение
- await self.send_or_edit_message(update, message_text, keyboard)
-
- def create_file_browser_keyboard(self, items: List[Dict], current_path: str,
- current_page: int, total_pages: int) -> InlineKeyboardMarkup:
- """Создает клавиатуру для просмотра файлов и папок."""
- keyboard = []
-
- # Добавляем кнопки для каждого элемента
- for item in items:
- name = item.get('name', 'Unknown')
- is_dir = item.get('isdir', False)
-
- if is_dir:
- # Формируем путь к подпапке
- folder_path = os.path.join(current_path, name).replace('\\', '/')
- if folder_path.endswith('//'):
- folder_path = folder_path[:-1]
-
- keyboard.append([
- InlineKeyboardButton(
- f"📁 {name}",
- callback_data=f"fm:browse:{folder_path}"
- )
- ])
- else:
- # Формируем путь к файлу
- file_path = os.path.join(current_path, name).replace('\\', '/')
- file_size = self.get_human_readable_size(item.get('size', 0))
-
- keyboard.append([
- InlineKeyboardButton(
- f"📄 {name} ({file_size})",
- callback_data=f"fm:download:{file_path}"
- )
- ])
-
- # Добавляем кнопки навигации
- nav_buttons = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- nav_buttons.append(InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}"))
-
- # Кнопки пагинации
- if total_pages > 1:
- nav_buttons.append(InlineKeyboardButton(
- "⬅️",
- callback_data=f"fm:nav:prev:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- f"{current_page + 1}/{total_pages}",
- callback_data=f"fm:nav:refresh:{current_path}"
- ))
- nav_buttons.append(InlineKeyboardButton(
- "➡️",
- callback_data=f"fm:nav:next:{current_path}"
- ))
-
- keyboard.append(nav_buttons)
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- def get_empty_folder_keyboard(self, current_path: str) -> InlineKeyboardMarkup:
- """Создает клавиатуру для пустой папки."""
- keyboard = []
-
- # Кнопка "Вверх", если не в корневой директории
- if current_path != "/" and current_path:
- parent_path = os.path.dirname(current_path) or "/"
- keyboard.append([InlineKeyboardButton("⬆️ Вверх", callback_data=f"fm:browse:{parent_path}")])
-
- # Добавляем кнопки действий
- action_buttons = [
- InlineKeyboardButton("📤 Загрузить файл", callback_data=f"fm:upload:{current_path}"),
- InlineKeyboardButton("📁 Новая папка", callback_data=f"fm:mkdir:{current_path}")
- ]
- keyboard.append(action_buttons)
-
- # Кнопка закрытия
- keyboard.append([InlineKeyboardButton("🔚 Закрыть", callback_data="fm:nav:close")])
-
- return InlineKeyboardMarkup(keyboard)
-
- async def send_or_edit_message(self, update: Update, text: str, reply_markup: InlineKeyboardMarkup) -> None:
- """Отправляет новое сообщение или редактирует существующее."""
- if update.callback_query:
- await update.callback_query.answer()
- try:
- await update.callback_query.edit_message_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- except Exception as e:
- logger.error(f"Error editing message: {e}")
- if update.callback_query.message:
- await update.callback_query.message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
- elif update.message:
- await update.message.reply_text(
- text,
- reply_markup=reply_markup,
- parse_mode=ParseMode.HTML
- )
-
- async def browse_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает переходы по директориям."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:browse:")[1]
-
- # Устанавливаем новый путь для пользователя
- self.set_user_path(user_id, path)
- # Сбрасываем пагинацию
- self.set_user_pagination(user_id, 0, 1)
-
- # Отображаем содержимое нового пути
- await self.display_directory_content(update, context)
- return BROWSING
-
- async def download_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на скачивание файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- file_path = query.data.split("fm:download:")[1]
-
- # Информация о файле
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer(f"Подготовка к скачиванию {file_name}...")
-
- # Создаем клавиатуру с кнопками действий для файла
- keyboard = [
- [
- InlineKeyboardButton("⬇️ Скачать", callback_data=f"fm:download:get:{file_path}"),
- InlineKeyboardButton("❌ Удалить", callback_data=f"fm:delete:confirm:{file_path}")
- ],
- [
- InlineKeyboardButton("✏️ Переименовать", callback_data=f"fm:rename:start:{file_path}"),
- InlineKeyboardButton("🔙 Назад", callback_data=f"fm:browse:{file_dir}")
- ]
- ]
-
- # Получаем дополнительную информацию о файле
- file_info = self.synology_api.get_file_info(file_path)
-
- if file_info:
- file_size = self.get_human_readable_size(file_info.get('size', 0))
- file_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info.get('time', {}).get('mtime', 0)))
- file_owner = file_info.get('owner', {}).get('user', 'Unknown')
-
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n"
- f"💾 Размер: {file_size}\n"
- f"🕒 Изменён: {file_time}\n"
- f"👤 Владелец: {file_owner}\n\n"
- f"Выберите действие:"
- )
- else:
- message_text = (
- f"📄 Файл: {html.escape(file_name)}\n\n"
- f"📂 Расположение: {html.escape(file_dir)}\n\n"
- f"Выберите действие:"
- )
-
- await query.edit_message_text(
- message_text,
- reply_markup=InlineKeyboardMarkup(keyboard),
- parse_mode=ParseMode.HTML
- )
-
- return BROWSING
-
- async def upload_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Начинает процесс загрузки файла."""
- query = update.callback_query
- if not query:
- return BROWSING
-
- user_id = update.effective_user.id
- path = query.data.split("fm:upload:")[1]
-
- # Сохраняем путь для загрузки в данные пользователя
- self.set_user_path(user_id, path)
-
- await query.answer()
- await query.edit_message_text(
- f"📤 Загрузка файла\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, отправьте файл, который хотите загрузить, или нажмите 'Отмена'.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return UPLOADING
-
- async def handle_file_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает загрузку файла от пользователя."""
- if not update.effective_user:
- return UPLOADING
-
- user_id = update.effective_user.id
- upload_path = self.get_user_path(user_id)
-
- # Проверяем наличие сообщения и файла
- if not update.message:
- return UPLOADING
-
- if not update.message.document:
- await update.message.reply_text(
- "❌ Не найден файл для загрузки. Пожалуйста, отправьте файл."
- )
- return UPLOADING
-
- document = update.message.document
- file_name = document.file_name or f"file_{int(time.time())}"
-
- # Сообщение о начале загрузки
- status_message = await update.message.reply_text(
- f"⏳ Начинаем загрузку файла {file_name}..."
- )
-
- try:
- # Получаем файл
- file = await context.bot.get_file(document.file_id)
- file_path = os.path.join(upload_path, file_name).replace("\\", "/")
-
- # Временный путь для сохранения файла
- temp_file_path = f"temp_{user_id}_{int(time.time())}_{file_name}"
-
- # Скачиваем файл во временную директорию
- await file.download_to_drive(temp_file_path)
-
- # Загружаем файл на Synology NAS
- success = self.synology_api.upload_file(temp_file_path, file_path)
-
- # Удаляем временный файл
- if os.path.exists(temp_file_path):
- os.remove(temp_file_path)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {file_name} успешно загружен в {upload_path}"
- )
-
- # Показываем содержимое директории
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось загрузить файл {file_name}. Пожалуйста, попробуйте снова."
- )
- return UPLOADING
-
- except Exception as e:
- logger.error(f"Error uploading file: {e}")
- await status_message.edit_text(
- f"❌ Произошла ошибка при загрузке файла: {str(e)}"
- )
- return UPLOADING
-
- async def delete_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на удаление файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":confirm:" in callback_data:
- # Запрос на подтверждение удаления
- file_path = callback_data.split("fm:delete:confirm:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer()
- await query.edit_message_text(
- f"❗ Подтверждение удаления\n\n"
- f"Вы действительно хотите удалить файл {html.escape(file_name)}?",
- reply_markup=InlineKeyboardMarkup([
- [
- InlineKeyboardButton("✅ Да, удалить", callback_data=f"fm:delete:execute:{file_path}"),
- InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")
- ]
- ]),
- parse_mode=ParseMode.HTML
- )
- return DELETING
-
- elif ":execute:" in callback_data:
- # Выполнение удаления
- file_path = callback_data.split("fm:delete:execute:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- await query.answer("Удаление файла...")
-
- # Удаляем файл
- success = self.synology_api.delete_file(file_path)
-
- if success:
- await query.edit_message_text(
- f"✅ Файл {file_name} успешно удален.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
- else:
- await query.edit_message_text(
- f"❌ Не удалось удалить файл {file_name}.",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("🔙 К папке", callback_data=f"fm:browse:{file_dir}")]
- ])
- )
-
- # Возвращаемся к просмотру директории
- return BROWSING
-
- return BROWSING
-
- async def rename_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на переименование файлов."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- # Извлекаем путь и режим из callback_data
- callback_data = query.data
- if ":start:" in callback_data:
- # Начало процесса переименования
- file_path = callback_data.split("fm:rename:start:")[1]
- file_name = os.path.basename(file_path)
- file_dir = os.path.dirname(file_path)
-
- # Сохраняем информацию о переименовании в контексте пользователя
- if hasattr(context, 'user_data') and context.user_data is not None:
- context.user_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
- # Дополнительно сохраняем в chat_data для надежности
- if hasattr(context, 'chat_data') and context.chat_data is not None:
- context.chat_data['renaming'] = {
- 'file_path': file_path,
- 'file_dir': file_dir
- }
-
- await query.answer()
- await query.edit_message_text(
- f"✏️ Переименование файла\n\n"
- f"Текущее имя: {html.escape(file_name)}\n\n"
- f"Пожалуйста, отправьте новое имя для файла:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{file_dir}")]
- ]),
- parse_mode=ParseMode.HTML
- )
- return RENAMING
-
- return BROWSING
-
- async def handle_rename(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает ввод нового имени файла."""
- if not update.message:
- return BROWSING
-
- # Проверяем где может быть информация о файле - в user_data или в chat_data
- file_path = None
- file_dir = None
-
- # Сначала проверяем user_data
- if hasattr(context, 'user_data') and context.user_data is not None and 'renaming' in context.user_data:
- file_path = context.user_data['renaming'].get('file_path')
- file_dir = context.user_data['renaming'].get('file_dir')
-
- # Если не нашли в user_data, проверяем в chat_data
- if (file_path is None or file_dir is None) and hasattr(context, 'chat_data') and context.chat_data is not None and 'renaming' in context.chat_data:
- file_path = context.chat_data['renaming'].get('file_path')
- file_dir = context.chat_data['renaming'].get('file_dir')
-
- if file_path is None or file_dir is None:
- await update.message.reply_text(
- "❌ Ошибка: информация о переименовании файла отсутствует."
- )
- return BROWSING
- old_name = os.path.basename(file_path)
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите текст для имени файла."
- )
- return RENAMING
-
- new_name = update.message.text.strip()
-
- # Проверяем корректность имени файла
- if not new_name or '/' in new_name or '\\' in new_name or new_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя файла. Пожалуйста, введите корректное имя без специальных символов."
- )
- return RENAMING
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Переименование {old_name} в {new_name}..."
- )
-
- # Переименовываем файл
- success = self.synology_api.rename_file(file_path, new_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Файл {old_name} успешно переименован в {new_name}"
- )
-
- # Очищаем данные о переименовании
- if hasattr(context, 'user_data') and context.user_data is not None and 'renaming' in context.user_data:
- del context.user_data['renaming']
-
- if hasattr(context, 'chat_data') and context.chat_data is not None and 'renaming' in context.chat_data:
- del context.chat_data['renaming']
-
- # Устанавливаем путь к директории и отображаем её содержимое
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- self.set_user_path(user_id, file_dir)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось переименовать файл {old_name}. Проверьте права доступа и допустимость имени."
- )
- return RENAMING
-
- async def create_folder_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает запросы на создание папок."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- path = query.data.split("fm:mkdir:")[1]
-
- # В PTB 20+ user_data должен быть всегда доступен
- # Просто добавляем нашу информацию в словарь
- # Если context.user_data не инициализирован, используем setdefault
- # чтобы добавить ключ, если его нет
- if hasattr(context, 'user_data') and context.user_data is not None:
- context.user_data['creating_folder'] = {
- 'path': path
- }
- else:
- # Если по какой-то причине user_data недоступен,
- # запишем путь в context.chat_data (он более стабилен)
- if hasattr(context, 'chat_data') and context.chat_data is not None:
- context.chat_data['creating_folder'] = {
- 'path': path
- }
-
- await query.answer()
- await query.edit_message_text(
- f"📁 Создание новой папки\n\n"
- f"Путь: {html.escape(path)}\n\n"
- f"Пожалуйста, введите имя для новой папки:",
- reply_markup=InlineKeyboardMarkup([
- [InlineKeyboardButton("❌ Отмена", callback_data=f"fm:browse:{path}")]
- ]),
- parse_mode=ParseMode.HTML
- )
-
- return CREATING_FOLDER
-
- async def handle_create_folder(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает создание новой папки."""
- if not update.message:
- return CREATING_FOLDER
-
- # Проверяем где может быть информация о папке - в user_data или в chat_data
- parent_path = None
-
- # Сначала проверяем user_data
- if hasattr(context, 'user_data') and context.user_data is not None:
- if 'creating_folder' in context.user_data:
- parent_path = context.user_data['creating_folder'].get('path')
-
- # Если не нашли в user_data, проверяем в chat_data
- if parent_path is None and hasattr(context, 'chat_data') and context.chat_data is not None:
- if 'creating_folder' in context.chat_data:
- parent_path = context.chat_data['creating_folder'].get('path')
-
- if parent_path is None:
- await update.message.reply_text(
- "❌ Ошибка: информация о создаваемой папке отсутствует."
- )
- return BROWSING
-
- if not update.message.text:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя."
- )
- return CREATING_FOLDER
-
- folder_name = update.message.text.strip()
-
- # Проверяем корректность имени папки
- if not folder_name or '/' in folder_name or '\\' in folder_name or folder_name in ['.', '..']:
- await update.message.reply_text(
- "❌ Некорректное имя папки. Пожалуйста, введите корректное имя без специальных символов."
- )
- return CREATING_FOLDER
-
- # Сообщение о начале операции
- status_message = await update.message.reply_text(
- f"⏳ Создание папки {folder_name}..."
- )
-
- # Создаем папку
- success = self.synology_api.create_folder(parent_path, folder_name)
-
- if success:
- await status_message.edit_text(
- f"✅ Папка {folder_name} успешно создана в {parent_path}"
- )
-
- # Очищаем данные о создании папки
- if hasattr(context, 'user_data') and context.user_data is not None and 'creating_folder' in context.user_data:
- del context.user_data['creating_folder']
-
- if hasattr(context, 'chat_data') and context.chat_data is not None and 'creating_folder' in context.chat_data:
- del context.chat_data['creating_folder']
-
- # Отображаем обновленное содержимое директории
- if not update.effective_user:
- return BROWSING
-
- user_id = update.effective_user.id
- self.set_user_path(user_id, parent_path)
- await self.display_directory_content(update, context)
- return BROWSING
- else:
- await status_message.edit_text(
- f"❌ Не удалось создать папку {folder_name}. Проверьте права доступа и допустимость имени."
- )
- return CREATING_FOLDER
-
- async def navigation_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обрабатывает навигационные запросы (пагинация, обновление, закрытие)."""
- query = update.callback_query
- if not query or not query.data:
- return BROWSING
-
- callback_data = query.data
- user_id = update.effective_user.id if update.effective_user else 0
-
- if callback_data.startswith("fm:nav:prev:"):
- # Предыдущая страница
- path = callback_data[len("fm:nav:prev:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] - 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:next:"):
- # Следующая страница
- path = callback_data[len("fm:nav:next:"):]
- pagination = self.get_user_pagination(user_id)
- page = (pagination['page'] + 1) % pagination['total_pages']
- self.set_user_pagination(user_id, page, pagination['total_pages'])
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data.startswith("fm:nav:refresh:"):
- # Обновить текущую директорию
- path = callback_data[len("fm:nav:refresh:"):]
- self.set_user_path(user_id, path)
- await self.display_directory_content(update, context)
-
- elif callback_data == "fm:nav:close":
- # Закрыть файловый менеджер
- await query.answer("Файловый менеджер закрыт")
- await query.delete_message()
- return ConversationHandler.END
-
- return BROWSING
-
- def get_human_readable_size(self, size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат."""
- if size_bytes == 0:
- return "0 B"
-
- size_names = ["B", "KB", "MB", "GB", "TB", "PB"]
- i = 0
- size_float = float(size_bytes)
- while size_float >= 1024 and i < len(size_names) - 1:
- size_float /= 1024.0
- i += 1
-
- return f"{size_float:.2f} {size_names[i]}"
-
-# Функция для создания ConversationHandler для файлового менеджера
-async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
- """Обработчик отмены диалога."""
- if update.message:
- await update.message.reply_text("Операция отменена.")
- return ConversationHandler.END
-
-def create_file_manager_handler(synology_api: SynologyAPI) -> ConversationHandler:
- """Создает и возвращает ConversationHandler для файлового менеджера."""
- file_manager = FileManagerAgent(synology_api)
-
- return ConversationHandler(
- entry_points=[CommandHandler("files", file_manager.start_file_manager)],
- states={
- BROWSING: [
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:"),
- CallbackQueryHandler(file_manager.download_callback, pattern="^fm:download:"),
- CallbackQueryHandler(file_manager.upload_callback, pattern="^fm:upload:"),
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.rename_callback, pattern="^fm:rename:"),
- CallbackQueryHandler(file_manager.create_folder_callback, pattern="^fm:mkdir:"),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:")
- ],
- UPLOADING: [
- MessageHandler(filters.ATTACHMENT & ~filters.COMMAND, file_manager.handle_file_upload),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- RENAMING: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_rename),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- DELETING: [
- CallbackQueryHandler(file_manager.delete_callback, pattern="^fm:delete:"),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ],
- CREATING_FOLDER: [
- MessageHandler(filters.TEXT & ~filters.COMMAND, file_manager.handle_create_folder),
- CallbackQueryHandler(file_manager.browse_callback, pattern="^fm:browse:")
- ]
- },
- fallbacks=[
- CommandHandler("cancel", cancel_conversation),
- CallbackQueryHandler(file_manager.navigation_callback, pattern="^fm:nav:close")
- ],
- name="file_manager",
- persistent=False
- )
diff --git a/.history/src/api/api_discovery_20250830081819.py b/.history/src/api/api_discovery_20250830081819.py
deleted file mode 100644
index 641dc8c..0000000
--- a/.history/src/api/api_discovery_20250830081819.py
+++ /dev/null
@@ -1,113 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для обнаружения доступных API Synology NAS
-"""
-
-import logging
-import requests
-import urllib3
-from typing import Dict, Any, List, Optional
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-def discover_available_apis(base_url: str, timeout=(10, 20), verify=False) -> Dict[str, Any]:
- """
- Получение списка доступных API на Synology NAS
-
- Args:
- base_url: базовый URL для API (например, 'http://192.168.0.100:5000/webapi')
- timeout: таймаут для запроса
- verify: проверять ли SSL-сертификат
-
- Returns:
- Словарь с информацией о доступных API
- """
- logger.info("Discovering available Synology APIs")
-
- try:
- # Делаем базовый запрос для получения всех доступных API
- api_info_url = f"{base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "all"
- }
-
- response = requests.get(
- api_info_url,
- params=api_info_params,
- timeout=timeout,
- verify=verify
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- apis = data.get("data", {})
- logger.info(f"Discovered {len(apis)} APIs")
-
- # Выводим список найденных API
- api_list = list(apis.keys())
- logger.debug(f"Available APIs: {', '.join(api_list[:10])}... and {len(api_list) - 10} more")
-
- # Группируем API по категориям
- power_apis = [api for api in api_list if "power" in api.lower()]
- system_apis = [api for api in api_list if "system" in api.lower()]
- info_apis = [api for api in api_list if "info" in api.lower()]
-
- logger.info(f"Power related APIs: {power_apis}")
- logger.info(f"System related APIs: {system_apis[:10]}")
- logger.info(f"Info related APIs: {info_apis[:10]}")
-
- return apis
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to discover APIs. Error code: {error_code}")
- return {}
- else:
- logger.error(f"API discovery request failed with HTTP status: {response.status_code}")
- return {}
- except Exception as e:
- logger.error(f"Error during API discovery: {str(e)}")
- return {}
-
-def find_compatible_api(apis: Dict[str, Any], api_category: str, method: str) -> List[Dict[str, Any]]:
- """
- Поиск совместимых API заданной категории
-
- Args:
- apis: словарь с доступными API
- api_category: категория API (например, 'system', 'power', 'info')
- method: искомый метод API
-
- Returns:
- Список подходящих API с версиями
- """
- compatible_apis = []
-
- for api_name, api_info in apis.items():
- if api_category.lower() in api_name.lower():
- compatible_apis.append({
- "name": api_name,
- "path": api_info.get("path", "entry.cgi"),
- "min_version": api_info.get("minVersion", 1),
- "max_version": api_info.get("maxVersion", 1),
- "method": method,
- "version": api_info.get("maxVersion", 1) # Используем максимальную версию по умолчанию
- })
-
- # Сортируем по приоритету
- compatible_apis.sort(key=lambda x: (
- # Приоритет по точности совпадения категории
- 0 if api_category.upper() in x["name"] else 1,
- # Приоритет по версии (от большей к меньшей)
- -x["max_version"]
- ))
-
- return compatible_apis
diff --git a/.history/src/api/api_discovery_20250830081957.py b/.history/src/api/api_discovery_20250830081957.py
deleted file mode 100644
index 641dc8c..0000000
--- a/.history/src/api/api_discovery_20250830081957.py
+++ /dev/null
@@ -1,113 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для обнаружения доступных API Synology NAS
-"""
-
-import logging
-import requests
-import urllib3
-from typing import Dict, Any, List, Optional
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-def discover_available_apis(base_url: str, timeout=(10, 20), verify=False) -> Dict[str, Any]:
- """
- Получение списка доступных API на Synology NAS
-
- Args:
- base_url: базовый URL для API (например, 'http://192.168.0.100:5000/webapi')
- timeout: таймаут для запроса
- verify: проверять ли SSL-сертификат
-
- Returns:
- Словарь с информацией о доступных API
- """
- logger.info("Discovering available Synology APIs")
-
- try:
- # Делаем базовый запрос для получения всех доступных API
- api_info_url = f"{base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "all"
- }
-
- response = requests.get(
- api_info_url,
- params=api_info_params,
- timeout=timeout,
- verify=verify
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- apis = data.get("data", {})
- logger.info(f"Discovered {len(apis)} APIs")
-
- # Выводим список найденных API
- api_list = list(apis.keys())
- logger.debug(f"Available APIs: {', '.join(api_list[:10])}... and {len(api_list) - 10} more")
-
- # Группируем API по категориям
- power_apis = [api for api in api_list if "power" in api.lower()]
- system_apis = [api for api in api_list if "system" in api.lower()]
- info_apis = [api for api in api_list if "info" in api.lower()]
-
- logger.info(f"Power related APIs: {power_apis}")
- logger.info(f"System related APIs: {system_apis[:10]}")
- logger.info(f"Info related APIs: {info_apis[:10]}")
-
- return apis
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to discover APIs. Error code: {error_code}")
- return {}
- else:
- logger.error(f"API discovery request failed with HTTP status: {response.status_code}")
- return {}
- except Exception as e:
- logger.error(f"Error during API discovery: {str(e)}")
- return {}
-
-def find_compatible_api(apis: Dict[str, Any], api_category: str, method: str) -> List[Dict[str, Any]]:
- """
- Поиск совместимых API заданной категории
-
- Args:
- apis: словарь с доступными API
- api_category: категория API (например, 'system', 'power', 'info')
- method: искомый метод API
-
- Returns:
- Список подходящих API с версиями
- """
- compatible_apis = []
-
- for api_name, api_info in apis.items():
- if api_category.lower() in api_name.lower():
- compatible_apis.append({
- "name": api_name,
- "path": api_info.get("path", "entry.cgi"),
- "min_version": api_info.get("minVersion", 1),
- "max_version": api_info.get("maxVersion", 1),
- "method": method,
- "version": api_info.get("maxVersion", 1) # Используем максимальную версию по умолчанию
- })
-
- # Сортируем по приоритету
- compatible_apis.sort(key=lambda x: (
- # Приоритет по точности совпадения категории
- 0 if api_category.upper() in x["name"] else 1,
- # Приоритет по версии (от большей к меньшей)
- -x["max_version"]
- ))
-
- return compatible_apis
diff --git a/.history/src/api/api_version_resolver_20250830084129.py b/.history/src/api/api_version_resolver_20250830084129.py
deleted file mode 100644
index 64a4003..0000000
--- a/.history/src/api/api_version_resolver_20250830084129.py
+++ /dev/null
@@ -1,234 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для разрешения проблем с API Synology и автоматического выбора совместимых версий
-"""
-
-import logging
-import requests
-from typing import Dict, Any, Optional, List, Tuple
-
-logger = logging.getLogger(__name__)
-
-class ApiVersionResolver:
- """Класс для определения совместимых версий API и правильных методов"""
-
- def __init__(self, base_url: str, session: requests.Session, timeout: tuple = (10, 20)):
- """Инициализация класса ApiVersionResolver
-
- Args:
- base_url: Базовый URL API Synology NAS (например, http://192.168.0.102:5000/webapi)
- session: Сессия requests для повторного использования соединений
- timeout: Таймауты для запросов (connect_timeout, read_timeout)
- """
- self.base_url = base_url
- self.session = session
- self.timeout = timeout
- self.api_info_cache = {}
-
- def get_api_info(self, api_name: str) -> Dict[str, Any]:
- """Получает информацию об API из SYNO.API.Info
-
- Args:
- api_name: Имя API для запроса (например, SYNO.DSM.Info)
-
- Returns:
- Dict с информацией об API или пустой словарь в случае ошибки
- """
- # Проверяем наличие данных в кэше
- if api_name in self.api_info_cache:
- return self.api_info_cache[api_name]
-
- try:
- # Запрос информации об API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- logger.debug(f"Querying API info for {api_name}")
- response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.timeout,
- verify=False
- )
-
- if response.status_code != 200:
- logger.warning(f"API info request failed with status {response.status_code}")
- return {}
-
- data = response.json()
- if not data.get("success"):
- logger.warning(f"API info request unsuccessful for {api_name}")
- return {}
-
- # Извлекаем информацию о запрошенном API
- api_info = data.get("data", {}).get(api_name, {})
- if not api_info:
- logger.warning(f"API {api_name} not found in API info response")
- return {}
-
- # Кэшируем результат
- self.api_info_cache[api_name] = api_info
- logger.debug(f"API info for {api_name}: {api_info}")
- return api_info
-
- except Exception as e:
- logger.error(f"Error querying API info for {api_name}: {str(e)}")
- return {}
-
- def resolve_api_path(self, api_name: str) -> str:
- """Определяет путь для API
-
- Args:
- api_name: Имя API
-
- Returns:
- Путь к API или 'entry.cgi' по умолчанию
- """
- api_info = self.get_api_info(api_name)
- return api_info.get("path", "entry.cgi")
-
- def resolve_api_version(self, api_name: str, requested_version: int) -> int:
- """Определяет совместимую версию API
-
- Args:
- api_name: Имя API
- requested_version: Запрошенная версия API
-
- Returns:
- Совместимая версия API, которая будет работать
- """
- api_info = self.get_api_info(api_name)
- if not api_info:
- # Если нет информации, возвращаем запрошенную версию
- return requested_version
-
- min_version = api_info.get("minVersion", 1)
- max_version = api_info.get("maxVersion", requested_version)
-
- # Проверка, поддерживается ли запрошенная версия
- if requested_version < min_version:
- logger.warning(f"API version {requested_version} for {api_name} is below minimum {min_version}, using {min_version}")
- return min_version
- elif requested_version > max_version:
- logger.warning(f"API version {requested_version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- return max_version
-
- return requested_version
-
- def resolve_api_method(self, api_name: str) -> Dict[str, str]:
- """Определяет доступные методы для API
-
- Args:
- api_name: Имя API
-
- Returns:
- Словарь с типами методов и их правильными именами для данного API
- """
- # Возможные методы для разных типов API
- api_methods = {
- # Методы для информации о системе
- "SYNO.DSM.Info": {"info": "getinfo", "get": "getinfo"},
- "SYNO.Core.System": {"info": "info", "get": "info"},
- "SYNO.Core.System.Status": {"info": "get", "get": "get"},
- "SYNO.Core.System.Info": {"info": "get", "get": "get"},
-
- # Методы для управления питанием
- "SYNO.Core.Hardware.PowerRecovery": {
- "restart": "setPowerOnState",
- "reboot": "setPowerOnState",
- "shutdown": "setPowerOnState",
- "poweroff": "setPowerOnState"
- },
- "SYNO.Core.System.Power": {
- "restart": "restart",
- "reboot": "restart",
- "shutdown": "shutdown",
- "poweroff": "shutdown"
- },
- "SYNO.DSM.Power": {
- "restart": "reboot",
- "reboot": "reboot",
- "shutdown": "shutdown",
- "poweroff": "shutdown"
- },
- "SYNO.Core.Hardware.NeedReboot": {
- "restart": "reboot",
- "reboot": "reboot"
- }
- }
-
- return api_methods.get(api_name, {})
-
- def get_api_special_params(self, api_name: str, method: str) -> Dict[str, Any]:
- """Возвращает специальные параметры, которые требуются для определенного API
-
- Args:
- api_name: Имя API
- method: Метод API
-
- Returns:
- Словарь с параметрами для метода или пустой словарь
- """
- # Специфические параметры для определенных API
- special_params = {
- # Параметры для управления питанием
- "SYNO.Core.Hardware.PowerRecovery": {
- "setPowerOnState": {
- "restart": {"reboot": "true"},
- "reboot": {"reboot": "true"},
- "shutdown": {"state": "powerbtn"},
- "poweroff": {"state": "powerbtn"}
- }
- },
- # Другие специальные параметры для других API
- }
-
- api_params = special_params.get(api_name, {})
- method_params = api_params.get(method, {})
-
- # Если это метод управления питанием, возвращаем соответствующие параметры
- if isinstance(method_params, dict) and method in method_params:
- return method_params[method]
-
- return method_params
-
- def find_compatible_api_for_function(self, function_type: str) -> List[Tuple[str, str, int]]:
- """Находит совместимые API для определенного типа функций
-
- Args:
- function_type: Тип функции ('info', 'power', 'status', etc.)
-
- Returns:
- Список кортежей (api_name, method, version) в порядке приоритета
- """
- # Определяем API для каждого типа функции
- function_apis = {
- "info": [
- ("SYNO.DSM.Info", "getinfo", 2),
- ("SYNO.Core.System", "info", 1),
- ("SYNO.Core.System.Status", "get", 1),
- ("SYNO.Core.System.Info", "get", 1)
- ],
- "power_restart": [
- ("SYNO.Core.Hardware.PowerRecovery", "setPowerOnState", 1),
- ("SYNO.Core.Hardware.NeedReboot", "reboot", 1),
- ("SYNO.Core.System.Power", "restart", 1),
- ("SYNO.DSM.Power", "reboot", 1),
- ("SYNO.Core.System", "reboot", 3)
- ],
- "power_shutdown": [
- ("SYNO.Core.Hardware.PowerRecovery", "setPowerOnState", 1),
- ("SYNO.Core.System.Power", "shutdown", 1),
- ("SYNO.DSM.Power", "shutdown", 1),
- ("SYNO.Core.System", "shutdown", 3)
- ]
- }
-
- return function_apis.get(function_type, [])
diff --git a/.history/src/api/api_version_resolver_20250830084257.py b/.history/src/api/api_version_resolver_20250830084257.py
deleted file mode 100644
index 64a4003..0000000
--- a/.history/src/api/api_version_resolver_20250830084257.py
+++ /dev/null
@@ -1,234 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для разрешения проблем с API Synology и автоматического выбора совместимых версий
-"""
-
-import logging
-import requests
-from typing import Dict, Any, Optional, List, Tuple
-
-logger = logging.getLogger(__name__)
-
-class ApiVersionResolver:
- """Класс для определения совместимых версий API и правильных методов"""
-
- def __init__(self, base_url: str, session: requests.Session, timeout: tuple = (10, 20)):
- """Инициализация класса ApiVersionResolver
-
- Args:
- base_url: Базовый URL API Synology NAS (например, http://192.168.0.102:5000/webapi)
- session: Сессия requests для повторного использования соединений
- timeout: Таймауты для запросов (connect_timeout, read_timeout)
- """
- self.base_url = base_url
- self.session = session
- self.timeout = timeout
- self.api_info_cache = {}
-
- def get_api_info(self, api_name: str) -> Dict[str, Any]:
- """Получает информацию об API из SYNO.API.Info
-
- Args:
- api_name: Имя API для запроса (например, SYNO.DSM.Info)
-
- Returns:
- Dict с информацией об API или пустой словарь в случае ошибки
- """
- # Проверяем наличие данных в кэше
- if api_name in self.api_info_cache:
- return self.api_info_cache[api_name]
-
- try:
- # Запрос информации об API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- logger.debug(f"Querying API info for {api_name}")
- response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.timeout,
- verify=False
- )
-
- if response.status_code != 200:
- logger.warning(f"API info request failed with status {response.status_code}")
- return {}
-
- data = response.json()
- if not data.get("success"):
- logger.warning(f"API info request unsuccessful for {api_name}")
- return {}
-
- # Извлекаем информацию о запрошенном API
- api_info = data.get("data", {}).get(api_name, {})
- if not api_info:
- logger.warning(f"API {api_name} not found in API info response")
- return {}
-
- # Кэшируем результат
- self.api_info_cache[api_name] = api_info
- logger.debug(f"API info for {api_name}: {api_info}")
- return api_info
-
- except Exception as e:
- logger.error(f"Error querying API info for {api_name}: {str(e)}")
- return {}
-
- def resolve_api_path(self, api_name: str) -> str:
- """Определяет путь для API
-
- Args:
- api_name: Имя API
-
- Returns:
- Путь к API или 'entry.cgi' по умолчанию
- """
- api_info = self.get_api_info(api_name)
- return api_info.get("path", "entry.cgi")
-
- def resolve_api_version(self, api_name: str, requested_version: int) -> int:
- """Определяет совместимую версию API
-
- Args:
- api_name: Имя API
- requested_version: Запрошенная версия API
-
- Returns:
- Совместимая версия API, которая будет работать
- """
- api_info = self.get_api_info(api_name)
- if not api_info:
- # Если нет информации, возвращаем запрошенную версию
- return requested_version
-
- min_version = api_info.get("minVersion", 1)
- max_version = api_info.get("maxVersion", requested_version)
-
- # Проверка, поддерживается ли запрошенная версия
- if requested_version < min_version:
- logger.warning(f"API version {requested_version} for {api_name} is below minimum {min_version}, using {min_version}")
- return min_version
- elif requested_version > max_version:
- logger.warning(f"API version {requested_version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- return max_version
-
- return requested_version
-
- def resolve_api_method(self, api_name: str) -> Dict[str, str]:
- """Определяет доступные методы для API
-
- Args:
- api_name: Имя API
-
- Returns:
- Словарь с типами методов и их правильными именами для данного API
- """
- # Возможные методы для разных типов API
- api_methods = {
- # Методы для информации о системе
- "SYNO.DSM.Info": {"info": "getinfo", "get": "getinfo"},
- "SYNO.Core.System": {"info": "info", "get": "info"},
- "SYNO.Core.System.Status": {"info": "get", "get": "get"},
- "SYNO.Core.System.Info": {"info": "get", "get": "get"},
-
- # Методы для управления питанием
- "SYNO.Core.Hardware.PowerRecovery": {
- "restart": "setPowerOnState",
- "reboot": "setPowerOnState",
- "shutdown": "setPowerOnState",
- "poweroff": "setPowerOnState"
- },
- "SYNO.Core.System.Power": {
- "restart": "restart",
- "reboot": "restart",
- "shutdown": "shutdown",
- "poweroff": "shutdown"
- },
- "SYNO.DSM.Power": {
- "restart": "reboot",
- "reboot": "reboot",
- "shutdown": "shutdown",
- "poweroff": "shutdown"
- },
- "SYNO.Core.Hardware.NeedReboot": {
- "restart": "reboot",
- "reboot": "reboot"
- }
- }
-
- return api_methods.get(api_name, {})
-
- def get_api_special_params(self, api_name: str, method: str) -> Dict[str, Any]:
- """Возвращает специальные параметры, которые требуются для определенного API
-
- Args:
- api_name: Имя API
- method: Метод API
-
- Returns:
- Словарь с параметрами для метода или пустой словарь
- """
- # Специфические параметры для определенных API
- special_params = {
- # Параметры для управления питанием
- "SYNO.Core.Hardware.PowerRecovery": {
- "setPowerOnState": {
- "restart": {"reboot": "true"},
- "reboot": {"reboot": "true"},
- "shutdown": {"state": "powerbtn"},
- "poweroff": {"state": "powerbtn"}
- }
- },
- # Другие специальные параметры для других API
- }
-
- api_params = special_params.get(api_name, {})
- method_params = api_params.get(method, {})
-
- # Если это метод управления питанием, возвращаем соответствующие параметры
- if isinstance(method_params, dict) and method in method_params:
- return method_params[method]
-
- return method_params
-
- def find_compatible_api_for_function(self, function_type: str) -> List[Tuple[str, str, int]]:
- """Находит совместимые API для определенного типа функций
-
- Args:
- function_type: Тип функции ('info', 'power', 'status', etc.)
-
- Returns:
- Список кортежей (api_name, method, version) в порядке приоритета
- """
- # Определяем API для каждого типа функции
- function_apis = {
- "info": [
- ("SYNO.DSM.Info", "getinfo", 2),
- ("SYNO.Core.System", "info", 1),
- ("SYNO.Core.System.Status", "get", 1),
- ("SYNO.Core.System.Info", "get", 1)
- ],
- "power_restart": [
- ("SYNO.Core.Hardware.PowerRecovery", "setPowerOnState", 1),
- ("SYNO.Core.Hardware.NeedReboot", "reboot", 1),
- ("SYNO.Core.System.Power", "restart", 1),
- ("SYNO.DSM.Power", "reboot", 1),
- ("SYNO.Core.System", "reboot", 3)
- ],
- "power_shutdown": [
- ("SYNO.Core.Hardware.PowerRecovery", "setPowerOnState", 1),
- ("SYNO.Core.System.Power", "shutdown", 1),
- ("SYNO.DSM.Power", "shutdown", 1),
- ("SYNO.Core.System", "shutdown", 3)
- ]
- }
-
- return function_apis.get(function_type, [])
diff --git a/.history/src/api/filestation_20250830141415.py b/.history/src/api/filestation_20250830141415.py
deleted file mode 100644
index ec73411..0000000
--- a/.history/src/api/filestation_20250830141415.py
+++ /dev/null
@@ -1,512 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с файловой системой Synology NAS через API FileStation
-"""
-
-import os
-import logging
-import requests
-from typing import Dict, Any, Optional, List, Union
-
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-def add_file_manager_methods_to_synology_api(api_class):
- """Добавляет методы для работы с файловой системой к классу SynologyAPI"""
-
- def list_files(self, folder_path: str = "/") -> List[Dict[str, Any]]:
- """Получение списка файлов и папок в указанной директории
-
- Args:
- folder_path: Путь к директории для просмотра
-
- Returns:
- Список файлов и папок в указанной директории
- """
- logger.info(f"Listing files in directory: {folder_path}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file listing")
- return []
-
- try:
- # Если это корневая папка, получаем список общих папок
- if folder_path == "/":
- result = self._make_api_request(
- "SYNO.FileStation.List",
- "list_share",
- version=2
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.List",
- "list_share",
- version=1
- )
-
- if not result:
- logger.error("Failed to list shared folders")
- return []
-
- return result.get("shares", [])
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request(
- "SYNO.FileStation.List",
- "list",
- version=2,
- params=params
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.List",
- "list",
- version=1,
- params=params
- )
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return []
-
- return result.get("files", [])
-
- except Exception as e:
- logger.error(f"Error listing files in {folder_path}: {str(e)}")
- return []
-
- def get_file_info(self, file_path: str) -> Dict[str, Any]:
- """Получение подробной информации о файле
-
- Args:
- file_path: Полный путь к файлу
-
- Returns:
- Информация о файле
- """
- logger.info(f"Getting file info: {file_path}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file info request")
- return {}
-
- try:
- params = {
- "path": file_path,
- "additional": "real_path,size,owner,time,perm"
- }
-
- result = self._make_api_request(
- "SYNO.FileStation.List",
- "getinfo",
- version=2,
- params=params
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.List",
- "getinfo",
- version=1,
- params=params
- )
-
- if not result:
- logger.error(f"Failed to get file info for {file_path}")
- return {}
-
- # Возвращаем информацию о первом файле в результате
- files = result.get("files", [])
- if files and len(files) > 0:
- return files[0]
-
- return {}
-
- except Exception as e:
- logger.error(f"Error getting file info for {file_path}: {str(e)}")
- return {}
-
- def download_file(self, file_path: str, local_path: str) -> bool:
- """Скачивание файла с NAS
-
- Args:
- file_path: Путь к файлу на NAS
- local_path: Локальный путь для сохранения файла
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Downloading file from {file_path} to {local_path}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file download")
- return False
-
- try:
- # Получаем URL для скачивания файла
- params = {
- "path": file_path,
- "mode": "download"
- }
-
- result = self._make_api_request(
- "SYNO.FileStation.Download",
- "download",
- version=2,
- params=params
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.Download",
- "download",
- version=1,
- params=params
- )
-
- if not result:
- logger.error(f"Failed to get download URL for {file_path}")
- return False
-
- # URL для скачивания
- download_url = result.get("url")
- if not download_url:
- logger.error("No download URL received")
- return False
-
- # Добавляем базовый URL, если URL относительный
- if not download_url.startswith("http"):
- protocol = "https" if self.protocol == "https" else "http"
- download_url = f"{protocol}://{self.base_url}/{download_url}"
-
- # Скачиваем файл
- response = self.session.get(download_url, stream=True, verify=False)
- if response.status_code != 200:
- logger.error(f"Failed to download file: HTTP {response.status_code}")
- return False
-
- # Сохраняем файл
- with open(local_path, 'wb') as f:
- for chunk in response.iter_content(chunk_size=8192):
- if chunk:
- f.write(chunk)
-
- logger.info(f"File successfully downloaded to {local_path}")
- return True
-
- except Exception as e:
- logger.error(f"Error downloading file {file_path}: {str(e)}")
- return False
-
- def upload_file(self, local_path: str, folder_path: str) -> bool:
- """Загрузка файла на NAS
-
- Args:
- local_path: Локальный путь к файлу
- folder_path: Путь на NAS для загрузки файла
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Uploading file from {local_path} to {folder_path}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file upload")
- return False
-
- try:
- # Проверяем существование файла
- if not os.path.exists(local_path):
- logger.error(f"Local file {local_path} not found")
- return False
-
- # Формируем URL для загрузки
- url = f"{self.base_url}/entry.cgi"
-
- # Извлекаем имя файла из локального пути
- file_name = os.path.basename(local_path)
-
- # Подготавливаем параметры для загрузки
- params = {
- "api": "SYNO.FileStation.Upload",
- "version": "2",
- "method": "upload",
- "path": folder_path,
- "_sid": self.sid
- }
-
- # Подготавливаем файл для загрузки
- files = {
- 'file': (file_name, open(local_path, 'rb'))
- }
-
- # Выполняем запрос
- response = self.session.post(url, params=params, files=files, verify=False)
-
- # Закрываем файл
- files['file'][1].close()
-
- if response.status_code != 200:
- logger.error(f"Failed to upload file: HTTP {response.status_code}")
- return False
-
- # Проверяем ответ
- try:
- data = response.json()
- success = data.get("success", False)
-
- if not success:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to upload file: Error code {error_code}")
- return False
-
- logger.info(f"File successfully uploaded to {folder_path}/{file_name}")
- return True
-
- except Exception as e:
- logger.error(f"Error parsing upload response: {str(e)}")
- return False
-
- except Exception as e:
- logger.error(f"Error uploading file {local_path}: {str(e)}")
- return False
-
- def delete_file(self, file_path: str) -> bool:
- """Удаление файла на NAS
-
- Args:
- file_path: Путь к файлу для удаления
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Deleting file: {file_path}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file deletion")
- return False
-
- try:
- # Подготавливаем параметры для удаления
- params = {
- "path": [file_path],
- "recursive": True # Удаляем папки рекурсивно
- }
-
- result = self._make_api_request(
- "SYNO.FileStation.Delete",
- "delete",
- version=2,
- params=params
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.Delete",
- "delete",
- version=1,
- params=params
- )
-
- if not result:
- logger.error(f"Failed to delete file {file_path}")
- return False
-
- # Проверяем результат
- task_id = result.get("taskid")
- if not task_id:
- logger.error("No task ID received for deletion")
- return False
-
- # Проверяем статус задачи
- task_params = {
- "taskid": task_id
- }
-
- # Ждем завершения задачи
- for _ in range(10):
- task_result = self._make_api_request(
- "SYNO.FileStation.Delete",
- "status",
- version=2,
- params=task_params
- )
-
- if not task_result:
- task_result = self._make_api_request(
- "SYNO.FileStation.Delete",
- "status",
- version=1,
- params=task_params
- )
-
- if not task_result:
- logger.error(f"Failed to check delete task status for {file_path}")
- return False
-
- # Проверяем статус задачи
- if task_result.get("finished", False):
- return True
-
- # Ждем немного
- import time
- time.sleep(0.5)
-
- logger.warning(f"Delete task did not complete in time for {file_path}")
- return True # Возвращаем True, т.к. задача запущена успешно
-
- except Exception as e:
- logger.error(f"Error deleting file {file_path}: {str(e)}")
- return False
-
- def create_folder(self, parent_path: str, folder_name: str) -> bool:
- """Создание новой папки на NAS
-
- Args:
- parent_path: Родительский путь для новой папки
- folder_name: Имя новой папки
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Creating folder {folder_name} in {parent_path}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for folder creation")
- return False
-
- try:
- # Подготавливаем параметры для создания папки
- params = {
- "folder_path": parent_path,
- "name": folder_name
- }
-
- result = self._make_api_request(
- "SYNO.FileStation.CreateFolder",
- "create",
- version=2,
- params=params
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.CreateFolder",
- "create",
- version=1,
- params=params
- )
-
- if not result:
- logger.error(f"Failed to create folder {folder_name} in {parent_path}")
- return False
-
- # Проверяем результат
- folders = result.get("folders", [])
- if not folders:
- logger.error("No folder information received after creation")
- return False
-
- logger.info(f"Folder {folder_name} created successfully in {parent_path}")
- return True
-
- except Exception as e:
- logger.error(f"Error creating folder {folder_name}: {str(e)}")
- return False
-
- def rename_file(self, file_path: str, new_name: str) -> bool:
- """Переименование файла или папки на NAS
-
- Args:
- file_path: Путь к файлу для переименования
- new_name: Новое имя файла (без пути)
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Renaming {file_path} to {new_name}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file renaming")
- return False
-
- try:
- # Получаем путь к родительской директории
- parent_path = os.path.dirname(file_path)
-
- # Подготавливаем параметры для переименования
- params = {
- "path": file_path,
- "name": new_name
- }
-
- result = self._make_api_request(
- "SYNO.FileStation.Rename",
- "rename",
- version=2,
- params=params
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.Rename",
- "rename",
- version=1,
- params=params
- )
-
- if not result:
- logger.error(f"Failed to rename {file_path} to {new_name}")
- return False
-
- # Проверяем результат
- files = result.get("files", [])
- if not files:
- logger.error("No file information received after renaming")
- return False
-
- logger.info(f"File {file_path} renamed to {new_name} successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error renaming file {file_path}: {str(e)}")
- return False
-
- # Добавляем все методы в класс API
- api_class.list_files = list_files
- api_class.get_file_info = get_file_info
- api_class.download_file = download_file
- api_class.upload_file = upload_file
- api_class.delete_file = delete_file
- api_class.create_folder = create_folder
- api_class.rename_file = rename_file
-
- return api_class
-
-# Добавляем методы для работы с файлами к классу SynologyAPI
-add_file_manager_methods_to_synology_api(SynologyAPI)
diff --git a/.history/src/api/filestation_20250830141957.py b/.history/src/api/filestation_20250830141957.py
deleted file mode 100644
index ec73411..0000000
--- a/.history/src/api/filestation_20250830141957.py
+++ /dev/null
@@ -1,512 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с файловой системой Synology NAS через API FileStation
-"""
-
-import os
-import logging
-import requests
-from typing import Dict, Any, Optional, List, Union
-
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-def add_file_manager_methods_to_synology_api(api_class):
- """Добавляет методы для работы с файловой системой к классу SynologyAPI"""
-
- def list_files(self, folder_path: str = "/") -> List[Dict[str, Any]]:
- """Получение списка файлов и папок в указанной директории
-
- Args:
- folder_path: Путь к директории для просмотра
-
- Returns:
- Список файлов и папок в указанной директории
- """
- logger.info(f"Listing files in directory: {folder_path}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file listing")
- return []
-
- try:
- # Если это корневая папка, получаем список общих папок
- if folder_path == "/":
- result = self._make_api_request(
- "SYNO.FileStation.List",
- "list_share",
- version=2
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.List",
- "list_share",
- version=1
- )
-
- if not result:
- logger.error("Failed to list shared folders")
- return []
-
- return result.get("shares", [])
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request(
- "SYNO.FileStation.List",
- "list",
- version=2,
- params=params
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.List",
- "list",
- version=1,
- params=params
- )
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return []
-
- return result.get("files", [])
-
- except Exception as e:
- logger.error(f"Error listing files in {folder_path}: {str(e)}")
- return []
-
- def get_file_info(self, file_path: str) -> Dict[str, Any]:
- """Получение подробной информации о файле
-
- Args:
- file_path: Полный путь к файлу
-
- Returns:
- Информация о файле
- """
- logger.info(f"Getting file info: {file_path}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file info request")
- return {}
-
- try:
- params = {
- "path": file_path,
- "additional": "real_path,size,owner,time,perm"
- }
-
- result = self._make_api_request(
- "SYNO.FileStation.List",
- "getinfo",
- version=2,
- params=params
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.List",
- "getinfo",
- version=1,
- params=params
- )
-
- if not result:
- logger.error(f"Failed to get file info for {file_path}")
- return {}
-
- # Возвращаем информацию о первом файле в результате
- files = result.get("files", [])
- if files and len(files) > 0:
- return files[0]
-
- return {}
-
- except Exception as e:
- logger.error(f"Error getting file info for {file_path}: {str(e)}")
- return {}
-
- def download_file(self, file_path: str, local_path: str) -> bool:
- """Скачивание файла с NAS
-
- Args:
- file_path: Путь к файлу на NAS
- local_path: Локальный путь для сохранения файла
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Downloading file from {file_path} to {local_path}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file download")
- return False
-
- try:
- # Получаем URL для скачивания файла
- params = {
- "path": file_path,
- "mode": "download"
- }
-
- result = self._make_api_request(
- "SYNO.FileStation.Download",
- "download",
- version=2,
- params=params
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.Download",
- "download",
- version=1,
- params=params
- )
-
- if not result:
- logger.error(f"Failed to get download URL for {file_path}")
- return False
-
- # URL для скачивания
- download_url = result.get("url")
- if not download_url:
- logger.error("No download URL received")
- return False
-
- # Добавляем базовый URL, если URL относительный
- if not download_url.startswith("http"):
- protocol = "https" if self.protocol == "https" else "http"
- download_url = f"{protocol}://{self.base_url}/{download_url}"
-
- # Скачиваем файл
- response = self.session.get(download_url, stream=True, verify=False)
- if response.status_code != 200:
- logger.error(f"Failed to download file: HTTP {response.status_code}")
- return False
-
- # Сохраняем файл
- with open(local_path, 'wb') as f:
- for chunk in response.iter_content(chunk_size=8192):
- if chunk:
- f.write(chunk)
-
- logger.info(f"File successfully downloaded to {local_path}")
- return True
-
- except Exception as e:
- logger.error(f"Error downloading file {file_path}: {str(e)}")
- return False
-
- def upload_file(self, local_path: str, folder_path: str) -> bool:
- """Загрузка файла на NAS
-
- Args:
- local_path: Локальный путь к файлу
- folder_path: Путь на NAS для загрузки файла
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Uploading file from {local_path} to {folder_path}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file upload")
- return False
-
- try:
- # Проверяем существование файла
- if not os.path.exists(local_path):
- logger.error(f"Local file {local_path} not found")
- return False
-
- # Формируем URL для загрузки
- url = f"{self.base_url}/entry.cgi"
-
- # Извлекаем имя файла из локального пути
- file_name = os.path.basename(local_path)
-
- # Подготавливаем параметры для загрузки
- params = {
- "api": "SYNO.FileStation.Upload",
- "version": "2",
- "method": "upload",
- "path": folder_path,
- "_sid": self.sid
- }
-
- # Подготавливаем файл для загрузки
- files = {
- 'file': (file_name, open(local_path, 'rb'))
- }
-
- # Выполняем запрос
- response = self.session.post(url, params=params, files=files, verify=False)
-
- # Закрываем файл
- files['file'][1].close()
-
- if response.status_code != 200:
- logger.error(f"Failed to upload file: HTTP {response.status_code}")
- return False
-
- # Проверяем ответ
- try:
- data = response.json()
- success = data.get("success", False)
-
- if not success:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to upload file: Error code {error_code}")
- return False
-
- logger.info(f"File successfully uploaded to {folder_path}/{file_name}")
- return True
-
- except Exception as e:
- logger.error(f"Error parsing upload response: {str(e)}")
- return False
-
- except Exception as e:
- logger.error(f"Error uploading file {local_path}: {str(e)}")
- return False
-
- def delete_file(self, file_path: str) -> bool:
- """Удаление файла на NAS
-
- Args:
- file_path: Путь к файлу для удаления
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Deleting file: {file_path}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file deletion")
- return False
-
- try:
- # Подготавливаем параметры для удаления
- params = {
- "path": [file_path],
- "recursive": True # Удаляем папки рекурсивно
- }
-
- result = self._make_api_request(
- "SYNO.FileStation.Delete",
- "delete",
- version=2,
- params=params
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.Delete",
- "delete",
- version=1,
- params=params
- )
-
- if not result:
- logger.error(f"Failed to delete file {file_path}")
- return False
-
- # Проверяем результат
- task_id = result.get("taskid")
- if not task_id:
- logger.error("No task ID received for deletion")
- return False
-
- # Проверяем статус задачи
- task_params = {
- "taskid": task_id
- }
-
- # Ждем завершения задачи
- for _ in range(10):
- task_result = self._make_api_request(
- "SYNO.FileStation.Delete",
- "status",
- version=2,
- params=task_params
- )
-
- if not task_result:
- task_result = self._make_api_request(
- "SYNO.FileStation.Delete",
- "status",
- version=1,
- params=task_params
- )
-
- if not task_result:
- logger.error(f"Failed to check delete task status for {file_path}")
- return False
-
- # Проверяем статус задачи
- if task_result.get("finished", False):
- return True
-
- # Ждем немного
- import time
- time.sleep(0.5)
-
- logger.warning(f"Delete task did not complete in time for {file_path}")
- return True # Возвращаем True, т.к. задача запущена успешно
-
- except Exception as e:
- logger.error(f"Error deleting file {file_path}: {str(e)}")
- return False
-
- def create_folder(self, parent_path: str, folder_name: str) -> bool:
- """Создание новой папки на NAS
-
- Args:
- parent_path: Родительский путь для новой папки
- folder_name: Имя новой папки
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Creating folder {folder_name} in {parent_path}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for folder creation")
- return False
-
- try:
- # Подготавливаем параметры для создания папки
- params = {
- "folder_path": parent_path,
- "name": folder_name
- }
-
- result = self._make_api_request(
- "SYNO.FileStation.CreateFolder",
- "create",
- version=2,
- params=params
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.CreateFolder",
- "create",
- version=1,
- params=params
- )
-
- if not result:
- logger.error(f"Failed to create folder {folder_name} in {parent_path}")
- return False
-
- # Проверяем результат
- folders = result.get("folders", [])
- if not folders:
- logger.error("No folder information received after creation")
- return False
-
- logger.info(f"Folder {folder_name} created successfully in {parent_path}")
- return True
-
- except Exception as e:
- logger.error(f"Error creating folder {folder_name}: {str(e)}")
- return False
-
- def rename_file(self, file_path: str, new_name: str) -> bool:
- """Переименование файла или папки на NAS
-
- Args:
- file_path: Путь к файлу для переименования
- new_name: Новое имя файла (без пути)
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Renaming {file_path} to {new_name}")
-
- # Аутентифицируемся если нужно
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file renaming")
- return False
-
- try:
- # Получаем путь к родительской директории
- parent_path = os.path.dirname(file_path)
-
- # Подготавливаем параметры для переименования
- params = {
- "path": file_path,
- "name": new_name
- }
-
- result = self._make_api_request(
- "SYNO.FileStation.Rename",
- "rename",
- version=2,
- params=params
- )
-
- if not result:
- # Пробуем версию 1
- result = self._make_api_request(
- "SYNO.FileStation.Rename",
- "rename",
- version=1,
- params=params
- )
-
- if not result:
- logger.error(f"Failed to rename {file_path} to {new_name}")
- return False
-
- # Проверяем результат
- files = result.get("files", [])
- if not files:
- logger.error("No file information received after renaming")
- return False
-
- logger.info(f"File {file_path} renamed to {new_name} successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error renaming file {file_path}: {str(e)}")
- return False
-
- # Добавляем все методы в класс API
- api_class.list_files = list_files
- api_class.get_file_info = get_file_info
- api_class.download_file = download_file
- api_class.upload_file = upload_file
- api_class.delete_file = delete_file
- api_class.create_folder = create_folder
- api_class.rename_file = rename_file
-
- return api_class
-
-# Добавляем методы для работы с файлами к классу SynologyAPI
-add_file_manager_methods_to_synology_api(SynologyAPI)
diff --git a/.history/src/api/synology_20250830063552.py b/.history/src/api/synology_20250830063552.py
deleted file mode 100644
index 1a03a0b..0000000
--- a/.history/src/api/synology_20250830063552.py
+++ /dev/null
@@ -1,262 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- if not self.sid and not self.login():
- return None
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data")
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
- return None
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return None
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
diff --git a/.history/src/api/synology_20250830063839.py b/.history/src/api/synology_20250830063839.py
deleted file mode 100644
index 1a03a0b..0000000
--- a/.history/src/api/synology_20250830063839.py
+++ /dev/null
@@ -1,262 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- if not self.sid and not self.login():
- return None
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data")
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
- return None
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return None
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
diff --git a/.history/src/api/synology_20250830065021.py b/.history/src/api/synology_20250830065021.py
deleted file mode 100644
index f361511..0000000
--- a/.history/src/api/synology_20250830065021.py
+++ /dev/null
@@ -1,267 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List, Tuple
-import socket
-import struct
-from time import sleep
-import urllib3
-from synology import SynologyDSM
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- if not self.sid and not self.login():
- return None
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data")
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
- return None
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return None
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
diff --git a/.history/src/api/synology_20250830065110.py b/.history/src/api/synology_20250830065110.py
deleted file mode 100644
index f830e5c..0000000
--- a/.history/src/api/synology_20250830065110.py
+++ /dev/null
@@ -1,315 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List, Tuple
-import socket
-import struct
-from time import sleep
-import urllib3
-from synology import SynologyDSM
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS с использованием python-synology"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.dsm = None
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS используя python-synology"""
- try:
- # Создаем экземпляр SynologyDSM
- self.dsm = SynologyDSM(
- SYNOLOGY_HOST,
- port=SYNOLOGY_PORT,
- username=SYNOLOGY_USERNAME,
- password=SYNOLOGY_PASSWORD,
- secure=SYNOLOGY_SECURE,
- timeout=SYNOLOGY_TIMEOUT,
- verify_ssl=False
- )
-
- # Авторизация
- self.dsm.login()
- logger.info("Successfully logged in to Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Failed to log in to Synology NAS: {str(e)}")
- self.dsm = None
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.dsm:
- return True
-
- try:
- self.dsm.logout()
- self.dsm = None
- logger.info("Successfully logged out from Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Error during logout: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение расширенного статуса системы"""
- if not self.dsm and not self.login():
- return None
-
- try:
- result = {
- "model": self.dsm.information.model,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime,
- "serial": self.dsm.information.serial,
- "temperature": self.dsm.information.temperature,
- "temperature_unit": "C",
- "cpu_usage": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": self._get_network_info(),
- "volumes": self._get_volumes_info()
- }
-
- logger.info("Successfully fetched extended system status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system status: {str(e)}")
- return None
-
- def _get_network_info(self) -> List[Dict[str, Any]]:
- """Получение информации о сетевых интерфейсах"""
- try:
- result = []
-
- # Получение информации о сети
- for device in self.dsm.network.interfaces:
- net_info = {
- "device": device,
- "ip": self.dsm.network.get_ip(device),
- "mask": self.dsm.network.get_mask(device),
- "mac": self.dsm.network.get_mac(device),
- "type": self.dsm.network.get_type(device),
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
- result.append(net_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting network info: {str(e)}")
- return []
-
- def _get_volumes_info(self) -> List[Dict[str, Any]]:
- """Получение информации о томах хранения"""
- try:
- result = []
-
- # Получение информации о томах
- for volume in self.dsm.storage.volumes:
- vol_info = {
- "name": volume,
- "status": self.dsm.storage.volume_status(volume),
- "device_type": self.dsm.storage.volume_device_type(volume),
- "total_size": self.dsm.storage.volume_size_total(volume),
- "used_size": self.dsm.storage.volume_size_used(volume),
- "percent_used": self.dsm.storage.volume_percentage_used(volume)
- }
- result.append(vol_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting volumes info: {str(e)}")
- return []
-
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- if not self.dsm and not self.login():
- return []
-
- try:
- result = []
-
- # Получение информации о общих папках
- for folder in self.dsm.share.shares:
- share_info = {
- "name": folder,
- "path": self.dsm.share.get_info(folder).get("path", ""),
- "desc": self.dsm.share.get_info(folder).get("desc", "")
- }
- result.append(share_info)
-
- logger.info(f"Successfully retrieved {len(result)} shared folders")
- return result
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_info(self) -> Dict[str, Any]:
- """Получение основной информации о системе"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "model": self.dsm.information.model,
- "serial": self.dsm.information.serial,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime
- }
-
- logger.info("Successfully fetched system info")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system info: {str(e)}")
- return {}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды выключения
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "shutdown"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system shutdown")
- return True
-
- except Exception as e:
- logger.error(f"Error shutting down system: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды перезагрузки
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "reboot"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system reboot")
- return True
-
- except Exception as e:
- logger.error(f"Error rebooting system: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
diff --git a/.history/src/api/synology_20250830065154.py b/.history/src/api/synology_20250830065154.py
deleted file mode 100644
index 9dc9610..0000000
--- a/.history/src/api/synology_20250830065154.py
+++ /dev/null
@@ -1,447 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List, Tuple
-import socket
-import struct
-from time import sleep
-import urllib3
-from synology import SynologyDSM
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS с использованием python-synology"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.dsm = None
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS используя python-synology"""
- try:
- # Создаем экземпляр SynologyDSM
- self.dsm = SynologyDSM(
- SYNOLOGY_HOST,
- port=SYNOLOGY_PORT,
- username=SYNOLOGY_USERNAME,
- password=SYNOLOGY_PASSWORD,
- secure=SYNOLOGY_SECURE,
- timeout=SYNOLOGY_TIMEOUT,
- verify_ssl=False
- )
-
- # Авторизация
- self.dsm.login()
- logger.info("Successfully logged in to Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Failed to log in to Synology NAS: {str(e)}")
- self.dsm = None
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.dsm:
- return True
-
- try:
- self.dsm.logout()
- self.dsm = None
- logger.info("Successfully logged out from Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Error during logout: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение расширенного статуса системы"""
- if not self.dsm and not self.login():
- return None
-
- try:
- result = {
- "model": self.dsm.information.model,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime,
- "serial": self.dsm.information.serial,
- "temperature": self.dsm.information.temperature,
- "temperature_unit": "C",
- "cpu_usage": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": self._get_network_info(),
- "volumes": self._get_volumes_info()
- }
-
- logger.info("Successfully fetched extended system status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system status: {str(e)}")
- return None
-
- def _get_network_info(self) -> List[Dict[str, Any]]:
- """Получение информации о сетевых интерфейсах"""
- try:
- result = []
-
- # Получение информации о сети
- for device in self.dsm.network.interfaces:
- net_info = {
- "device": device,
- "ip": self.dsm.network.get_ip(device),
- "mask": self.dsm.network.get_mask(device),
- "mac": self.dsm.network.get_mac(device),
- "type": self.dsm.network.get_type(device),
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
- result.append(net_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting network info: {str(e)}")
- return []
-
- def _get_volumes_info(self) -> List[Dict[str, Any]]:
- """Получение информации о томах хранения"""
- try:
- result = []
-
- # Получение информации о томах
- for volume in self.dsm.storage.volumes:
- vol_info = {
- "name": volume,
- "status": self.dsm.storage.volume_status(volume),
- "device_type": self.dsm.storage.volume_device_type(volume),
- "total_size": self.dsm.storage.volume_size_total(volume),
- "used_size": self.dsm.storage.volume_size_used(volume),
- "percent_used": self.dsm.storage.volume_percentage_used(volume)
- }
- result.append(vol_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting volumes info: {str(e)}")
- return []
-
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- if not self.dsm and not self.login():
- return []
-
- try:
- result = []
-
- # Получение информации о общих папках
- for folder in self.dsm.share.shares:
- share_info = {
- "name": folder,
- "path": self.dsm.share.get_info(folder).get("path", ""),
- "desc": self.dsm.share.get_info(folder).get("desc", "")
- }
- result.append(share_info)
-
- logger.info(f"Successfully retrieved {len(result)} shared folders")
- return result
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_info(self) -> Dict[str, Any]:
- """Получение основной информации о системе"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "model": self.dsm.information.model,
- "serial": self.dsm.information.serial,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime
- }
-
- logger.info("Successfully fetched system info")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system info: {str(e)}")
- return {}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды выключения
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "shutdown"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system shutdown")
- return True
-
- except Exception as e:
- logger.error(f"Error shutting down system: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды перезагрузки
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "reboot"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system reboot")
- return True
-
- except Exception as e:
- logger.error(f"Error rebooting system: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "cpu_load": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "cached_mb": self.dsm.utilisation.memory_cached_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": {}
- }
-
- # Добавляем данные по сети
- for device in self.dsm.network.interfaces:
- result["network"][device] = {
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
-
- logger.info("Successfully fetched system load")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "volumes": self._get_volumes_info(),
- "disks": self._get_disks_info(),
- "total_size": 0,
- "total_used": 0
- }
-
- # Суммируем общий размер и использование
- for volume in result["volumes"]:
- result["total_size"] += volume["total_size"]
- result["total_used"] += volume["used_size"]
-
- logger.info("Successfully fetched storage status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting storage status: {str(e)}")
- return {}
-
- def _get_disks_info(self) -> List[Dict[str, Any]]:
- """Получение информации о дисках"""
- try:
- result = []
-
- # Получение информации о дисках
- for disk in self.dsm.storage.disks:
- disk_info = {
- "name": disk,
- "model": self.dsm.storage.disk_model(disk),
- "type": self.dsm.storage.disk_type(disk),
- "status": self.dsm.storage.disk_status(disk),
- "temp": self.dsm.storage.disk_temp(disk)
- }
- result.append(disk_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting disks info: {str(e)}")
- return []
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности"""
- if not self.dsm and not self.login():
- return {"success": False}
-
- try:
- # Используем низкоуровневый API для получения информации о безопасности
- endpoint = "SYNO.Core.Security.DSM"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "status"}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response:
- return {
- "success": True,
- "status": response["data"].get("status", "unknown"),
- "last_check": response["data"].get("last_check", None),
- "is_secure": response["data"].get("is_secure", False)
- }
- else:
- return {"success": False}
-
- except Exception as e:
- logger.error(f"Error getting security status: {str(e)}")
- return {"success": False}
-
- def get_users(self) -> List[str]:
- """Получение списка пользователей"""
- if not self.dsm and not self.login():
- return []
-
- try:
- users = []
-
- # Используем низкоуровневый API для получения списка пользователей
- endpoint = "SYNO.Core.User"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "list", "additional": ["email"]}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response and "users" in response["data"]:
- for user in response["data"]["users"]:
- if "name" in user:
- users.append(user["name"])
-
- logger.info(f"Successfully retrieved {len(users)} users")
- return users
-
- except Exception as e:
- logger.error(f"Error getting users: {str(e)}")
- return []
diff --git a/.history/src/api/synology_20250830065454.py b/.history/src/api/synology_20250830065454.py
deleted file mode 100644
index 9dc9610..0000000
--- a/.history/src/api/synology_20250830065454.py
+++ /dev/null
@@ -1,447 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List, Tuple
-import socket
-import struct
-from time import sleep
-import urllib3
-from synology import SynologyDSM
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS с использованием python-synology"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.dsm = None
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS используя python-synology"""
- try:
- # Создаем экземпляр SynologyDSM
- self.dsm = SynologyDSM(
- SYNOLOGY_HOST,
- port=SYNOLOGY_PORT,
- username=SYNOLOGY_USERNAME,
- password=SYNOLOGY_PASSWORD,
- secure=SYNOLOGY_SECURE,
- timeout=SYNOLOGY_TIMEOUT,
- verify_ssl=False
- )
-
- # Авторизация
- self.dsm.login()
- logger.info("Successfully logged in to Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Failed to log in to Synology NAS: {str(e)}")
- self.dsm = None
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.dsm:
- return True
-
- try:
- self.dsm.logout()
- self.dsm = None
- logger.info("Successfully logged out from Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Error during logout: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение расширенного статуса системы"""
- if not self.dsm and not self.login():
- return None
-
- try:
- result = {
- "model": self.dsm.information.model,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime,
- "serial": self.dsm.information.serial,
- "temperature": self.dsm.information.temperature,
- "temperature_unit": "C",
- "cpu_usage": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": self._get_network_info(),
- "volumes": self._get_volumes_info()
- }
-
- logger.info("Successfully fetched extended system status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system status: {str(e)}")
- return None
-
- def _get_network_info(self) -> List[Dict[str, Any]]:
- """Получение информации о сетевых интерфейсах"""
- try:
- result = []
-
- # Получение информации о сети
- for device in self.dsm.network.interfaces:
- net_info = {
- "device": device,
- "ip": self.dsm.network.get_ip(device),
- "mask": self.dsm.network.get_mask(device),
- "mac": self.dsm.network.get_mac(device),
- "type": self.dsm.network.get_type(device),
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
- result.append(net_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting network info: {str(e)}")
- return []
-
- def _get_volumes_info(self) -> List[Dict[str, Any]]:
- """Получение информации о томах хранения"""
- try:
- result = []
-
- # Получение информации о томах
- for volume in self.dsm.storage.volumes:
- vol_info = {
- "name": volume,
- "status": self.dsm.storage.volume_status(volume),
- "device_type": self.dsm.storage.volume_device_type(volume),
- "total_size": self.dsm.storage.volume_size_total(volume),
- "used_size": self.dsm.storage.volume_size_used(volume),
- "percent_used": self.dsm.storage.volume_percentage_used(volume)
- }
- result.append(vol_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting volumes info: {str(e)}")
- return []
-
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- if not self.dsm and not self.login():
- return []
-
- try:
- result = []
-
- # Получение информации о общих папках
- for folder in self.dsm.share.shares:
- share_info = {
- "name": folder,
- "path": self.dsm.share.get_info(folder).get("path", ""),
- "desc": self.dsm.share.get_info(folder).get("desc", "")
- }
- result.append(share_info)
-
- logger.info(f"Successfully retrieved {len(result)} shared folders")
- return result
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_info(self) -> Dict[str, Any]:
- """Получение основной информации о системе"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "model": self.dsm.information.model,
- "serial": self.dsm.information.serial,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime
- }
-
- logger.info("Successfully fetched system info")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system info: {str(e)}")
- return {}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды выключения
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "shutdown"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system shutdown")
- return True
-
- except Exception as e:
- logger.error(f"Error shutting down system: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды перезагрузки
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "reboot"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system reboot")
- return True
-
- except Exception as e:
- logger.error(f"Error rebooting system: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "cpu_load": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "cached_mb": self.dsm.utilisation.memory_cached_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": {}
- }
-
- # Добавляем данные по сети
- for device in self.dsm.network.interfaces:
- result["network"][device] = {
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
-
- logger.info("Successfully fetched system load")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "volumes": self._get_volumes_info(),
- "disks": self._get_disks_info(),
- "total_size": 0,
- "total_used": 0
- }
-
- # Суммируем общий размер и использование
- for volume in result["volumes"]:
- result["total_size"] += volume["total_size"]
- result["total_used"] += volume["used_size"]
-
- logger.info("Successfully fetched storage status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting storage status: {str(e)}")
- return {}
-
- def _get_disks_info(self) -> List[Dict[str, Any]]:
- """Получение информации о дисках"""
- try:
- result = []
-
- # Получение информации о дисках
- for disk in self.dsm.storage.disks:
- disk_info = {
- "name": disk,
- "model": self.dsm.storage.disk_model(disk),
- "type": self.dsm.storage.disk_type(disk),
- "status": self.dsm.storage.disk_status(disk),
- "temp": self.dsm.storage.disk_temp(disk)
- }
- result.append(disk_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting disks info: {str(e)}")
- return []
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности"""
- if not self.dsm and not self.login():
- return {"success": False}
-
- try:
- # Используем низкоуровневый API для получения информации о безопасности
- endpoint = "SYNO.Core.Security.DSM"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "status"}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response:
- return {
- "success": True,
- "status": response["data"].get("status", "unknown"),
- "last_check": response["data"].get("last_check", None),
- "is_secure": response["data"].get("is_secure", False)
- }
- else:
- return {"success": False}
-
- except Exception as e:
- logger.error(f"Error getting security status: {str(e)}")
- return {"success": False}
-
- def get_users(self) -> List[str]:
- """Получение списка пользователей"""
- if not self.dsm and not self.login():
- return []
-
- try:
- users = []
-
- # Используем низкоуровневый API для получения списка пользователей
- endpoint = "SYNO.Core.User"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "list", "additional": ["email"]}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response and "users" in response["data"]:
- for user in response["data"]["users"]:
- if "name" in user:
- users.append(user["name"])
-
- logger.info(f"Successfully retrieved {len(users)} users")
- return users
-
- except Exception as e:
- logger.error(f"Error getting users: {str(e)}")
- return []
diff --git a/.history/src/api/synology_20250830071505.py b/.history/src/api/synology_20250830071505.py
deleted file mode 100644
index fe5d0a9..0000000
--- a/.history/src/api/synology_20250830071505.py
+++ /dev/null
@@ -1,447 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List, Tuple
-import socket
-import struct
-from time import sleep
-import urllib3
-from synology_dsm import SynologyDSM
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS с использованием python-synology"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.dsm = None
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS используя python-synology"""
- try:
- # Создаем экземпляр SynologyDSM
- self.dsm = SynologyDSM(
- SYNOLOGY_HOST,
- port=SYNOLOGY_PORT,
- username=SYNOLOGY_USERNAME,
- password=SYNOLOGY_PASSWORD,
- secure=SYNOLOGY_SECURE,
- timeout=SYNOLOGY_TIMEOUT,
- verify_ssl=False
- )
-
- # Авторизация
- self.dsm.login()
- logger.info("Successfully logged in to Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Failed to log in to Synology NAS: {str(e)}")
- self.dsm = None
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.dsm:
- return True
-
- try:
- self.dsm.logout()
- self.dsm = None
- logger.info("Successfully logged out from Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Error during logout: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение расширенного статуса системы"""
- if not self.dsm and not self.login():
- return None
-
- try:
- result = {
- "model": self.dsm.information.model,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime,
- "serial": self.dsm.information.serial,
- "temperature": self.dsm.information.temperature,
- "temperature_unit": "C",
- "cpu_usage": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": self._get_network_info(),
- "volumes": self._get_volumes_info()
- }
-
- logger.info("Successfully fetched extended system status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system status: {str(e)}")
- return None
-
- def _get_network_info(self) -> List[Dict[str, Any]]:
- """Получение информации о сетевых интерфейсах"""
- try:
- result = []
-
- # Получение информации о сети
- for device in self.dsm.network.interfaces:
- net_info = {
- "device": device,
- "ip": self.dsm.network.get_ip(device),
- "mask": self.dsm.network.get_mask(device),
- "mac": self.dsm.network.get_mac(device),
- "type": self.dsm.network.get_type(device),
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
- result.append(net_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting network info: {str(e)}")
- return []
-
- def _get_volumes_info(self) -> List[Dict[str, Any]]:
- """Получение информации о томах хранения"""
- try:
- result = []
-
- # Получение информации о томах
- for volume in self.dsm.storage.volumes:
- vol_info = {
- "name": volume,
- "status": self.dsm.storage.volume_status(volume),
- "device_type": self.dsm.storage.volume_device_type(volume),
- "total_size": self.dsm.storage.volume_size_total(volume),
- "used_size": self.dsm.storage.volume_size_used(volume),
- "percent_used": self.dsm.storage.volume_percentage_used(volume)
- }
- result.append(vol_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting volumes info: {str(e)}")
- return []
-
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- if not self.dsm and not self.login():
- return []
-
- try:
- result = []
-
- # Получение информации о общих папках
- for folder in self.dsm.share.shares:
- share_info = {
- "name": folder,
- "path": self.dsm.share.get_info(folder).get("path", ""),
- "desc": self.dsm.share.get_info(folder).get("desc", "")
- }
- result.append(share_info)
-
- logger.info(f"Successfully retrieved {len(result)} shared folders")
- return result
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_info(self) -> Dict[str, Any]:
- """Получение основной информации о системе"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "model": self.dsm.information.model,
- "serial": self.dsm.information.serial,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime
- }
-
- logger.info("Successfully fetched system info")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system info: {str(e)}")
- return {}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды выключения
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "shutdown"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system shutdown")
- return True
-
- except Exception as e:
- logger.error(f"Error shutting down system: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды перезагрузки
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "reboot"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system reboot")
- return True
-
- except Exception as e:
- logger.error(f"Error rebooting system: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "cpu_load": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "cached_mb": self.dsm.utilisation.memory_cached_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": {}
- }
-
- # Добавляем данные по сети
- for device in self.dsm.network.interfaces:
- result["network"][device] = {
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
-
- logger.info("Successfully fetched system load")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "volumes": self._get_volumes_info(),
- "disks": self._get_disks_info(),
- "total_size": 0,
- "total_used": 0
- }
-
- # Суммируем общий размер и использование
- for volume in result["volumes"]:
- result["total_size"] += volume["total_size"]
- result["total_used"] += volume["used_size"]
-
- logger.info("Successfully fetched storage status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting storage status: {str(e)}")
- return {}
-
- def _get_disks_info(self) -> List[Dict[str, Any]]:
- """Получение информации о дисках"""
- try:
- result = []
-
- # Получение информации о дисках
- for disk in self.dsm.storage.disks:
- disk_info = {
- "name": disk,
- "model": self.dsm.storage.disk_model(disk),
- "type": self.dsm.storage.disk_type(disk),
- "status": self.dsm.storage.disk_status(disk),
- "temp": self.dsm.storage.disk_temp(disk)
- }
- result.append(disk_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting disks info: {str(e)}")
- return []
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности"""
- if not self.dsm and not self.login():
- return {"success": False}
-
- try:
- # Используем низкоуровневый API для получения информации о безопасности
- endpoint = "SYNO.Core.Security.DSM"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "status"}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response:
- return {
- "success": True,
- "status": response["data"].get("status", "unknown"),
- "last_check": response["data"].get("last_check", None),
- "is_secure": response["data"].get("is_secure", False)
- }
- else:
- return {"success": False}
-
- except Exception as e:
- logger.error(f"Error getting security status: {str(e)}")
- return {"success": False}
-
- def get_users(self) -> List[str]:
- """Получение списка пользователей"""
- if not self.dsm and not self.login():
- return []
-
- try:
- users = []
-
- # Используем низкоуровневый API для получения списка пользователей
- endpoint = "SYNO.Core.User"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "list", "additional": ["email"]}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response and "users" in response["data"]:
- for user in response["data"]["users"]:
- if "name" in user:
- users.append(user["name"])
-
- logger.info(f"Successfully retrieved {len(users)} users")
- return users
-
- except Exception as e:
- logger.error(f"Error getting users: {str(e)}")
- return []
diff --git a/.history/src/api/synology_20250830071525.py b/.history/src/api/synology_20250830071525.py
deleted file mode 100644
index fe5d0a9..0000000
--- a/.history/src/api/synology_20250830071525.py
+++ /dev/null
@@ -1,447 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List, Tuple
-import socket
-import struct
-from time import sleep
-import urllib3
-from synology_dsm import SynologyDSM
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS с использованием python-synology"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.dsm = None
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS используя python-synology"""
- try:
- # Создаем экземпляр SynologyDSM
- self.dsm = SynologyDSM(
- SYNOLOGY_HOST,
- port=SYNOLOGY_PORT,
- username=SYNOLOGY_USERNAME,
- password=SYNOLOGY_PASSWORD,
- secure=SYNOLOGY_SECURE,
- timeout=SYNOLOGY_TIMEOUT,
- verify_ssl=False
- )
-
- # Авторизация
- self.dsm.login()
- logger.info("Successfully logged in to Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Failed to log in to Synology NAS: {str(e)}")
- self.dsm = None
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.dsm:
- return True
-
- try:
- self.dsm.logout()
- self.dsm = None
- logger.info("Successfully logged out from Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Error during logout: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение расширенного статуса системы"""
- if not self.dsm and not self.login():
- return None
-
- try:
- result = {
- "model": self.dsm.information.model,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime,
- "serial": self.dsm.information.serial,
- "temperature": self.dsm.information.temperature,
- "temperature_unit": "C",
- "cpu_usage": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": self._get_network_info(),
- "volumes": self._get_volumes_info()
- }
-
- logger.info("Successfully fetched extended system status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system status: {str(e)}")
- return None
-
- def _get_network_info(self) -> List[Dict[str, Any]]:
- """Получение информации о сетевых интерфейсах"""
- try:
- result = []
-
- # Получение информации о сети
- for device in self.dsm.network.interfaces:
- net_info = {
- "device": device,
- "ip": self.dsm.network.get_ip(device),
- "mask": self.dsm.network.get_mask(device),
- "mac": self.dsm.network.get_mac(device),
- "type": self.dsm.network.get_type(device),
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
- result.append(net_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting network info: {str(e)}")
- return []
-
- def _get_volumes_info(self) -> List[Dict[str, Any]]:
- """Получение информации о томах хранения"""
- try:
- result = []
-
- # Получение информации о томах
- for volume in self.dsm.storage.volumes:
- vol_info = {
- "name": volume,
- "status": self.dsm.storage.volume_status(volume),
- "device_type": self.dsm.storage.volume_device_type(volume),
- "total_size": self.dsm.storage.volume_size_total(volume),
- "used_size": self.dsm.storage.volume_size_used(volume),
- "percent_used": self.dsm.storage.volume_percentage_used(volume)
- }
- result.append(vol_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting volumes info: {str(e)}")
- return []
-
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- if not self.dsm and not self.login():
- return []
-
- try:
- result = []
-
- # Получение информации о общих папках
- for folder in self.dsm.share.shares:
- share_info = {
- "name": folder,
- "path": self.dsm.share.get_info(folder).get("path", ""),
- "desc": self.dsm.share.get_info(folder).get("desc", "")
- }
- result.append(share_info)
-
- logger.info(f"Successfully retrieved {len(result)} shared folders")
- return result
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_info(self) -> Dict[str, Any]:
- """Получение основной информации о системе"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "model": self.dsm.information.model,
- "serial": self.dsm.information.serial,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime
- }
-
- logger.info("Successfully fetched system info")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system info: {str(e)}")
- return {}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды выключения
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "shutdown"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system shutdown")
- return True
-
- except Exception as e:
- logger.error(f"Error shutting down system: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды перезагрузки
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "reboot"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system reboot")
- return True
-
- except Exception as e:
- logger.error(f"Error rebooting system: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "cpu_load": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "cached_mb": self.dsm.utilisation.memory_cached_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": {}
- }
-
- # Добавляем данные по сети
- for device in self.dsm.network.interfaces:
- result["network"][device] = {
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
-
- logger.info("Successfully fetched system load")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "volumes": self._get_volumes_info(),
- "disks": self._get_disks_info(),
- "total_size": 0,
- "total_used": 0
- }
-
- # Суммируем общий размер и использование
- for volume in result["volumes"]:
- result["total_size"] += volume["total_size"]
- result["total_used"] += volume["used_size"]
-
- logger.info("Successfully fetched storage status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting storage status: {str(e)}")
- return {}
-
- def _get_disks_info(self) -> List[Dict[str, Any]]:
- """Получение информации о дисках"""
- try:
- result = []
-
- # Получение информации о дисках
- for disk in self.dsm.storage.disks:
- disk_info = {
- "name": disk,
- "model": self.dsm.storage.disk_model(disk),
- "type": self.dsm.storage.disk_type(disk),
- "status": self.dsm.storage.disk_status(disk),
- "temp": self.dsm.storage.disk_temp(disk)
- }
- result.append(disk_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting disks info: {str(e)}")
- return []
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности"""
- if not self.dsm and not self.login():
- return {"success": False}
-
- try:
- # Используем низкоуровневый API для получения информации о безопасности
- endpoint = "SYNO.Core.Security.DSM"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "status"}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response:
- return {
- "success": True,
- "status": response["data"].get("status", "unknown"),
- "last_check": response["data"].get("last_check", None),
- "is_secure": response["data"].get("is_secure", False)
- }
- else:
- return {"success": False}
-
- except Exception as e:
- logger.error(f"Error getting security status: {str(e)}")
- return {"success": False}
-
- def get_users(self) -> List[str]:
- """Получение списка пользователей"""
- if not self.dsm and not self.login():
- return []
-
- try:
- users = []
-
- # Используем низкоуровневый API для получения списка пользователей
- endpoint = "SYNO.Core.User"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "list", "additional": ["email"]}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response and "users" in response["data"]:
- for user in response["data"]["users"]:
- if "name" in user:
- users.append(user["name"])
-
- logger.info(f"Successfully retrieved {len(users)} users")
- return users
-
- except Exception as e:
- logger.error(f"Error getting users: {str(e)}")
- return []
diff --git a/.history/src/api/synology_20250830071727.py b/.history/src/api/synology_20250830071727.py
deleted file mode 100644
index a351e5f..0000000
--- a/.history/src/api/synology_20250830071727.py
+++ /dev/null
@@ -1,446 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List, Tuple
-import socket
-import struct
-from time import sleep
-import urllib3
-from synology_dsm import SynologyDSM
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS с использованием python-synology"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.dsm = None
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS используя python-synology"""
- try:
- # Создаем экземпляр SynologyDSM
- self.dsm = SynologyDSM(
- dsm_ip=SYNOLOGY_HOST,
- dsm_port=SYNOLOGY_PORT,
- username=SYNOLOGY_USERNAME,
- password=SYNOLOGY_PASSWORD,
- use_https=SYNOLOGY_SECURE,
- timeout=SYNOLOGY_TIMEOUT,
- verify_ssl=False
- )
-
- # Авторизация
- self.dsm.login()
- logger.info("Successfully logged in to Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Failed to log in to Synology NAS: {str(e)}")
- self.dsm = None
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.dsm:
- return True
-
- try:
- self.dsm.logout()
- self.dsm = None
- logger.info("Successfully logged out from Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Error during logout: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение расширенного статуса системы"""
- if not self.dsm and not self.login():
- return None
-
- try:
- result = {
- "model": self.dsm.information.model,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime,
- "serial": self.dsm.information.serial,
- "temperature": self.dsm.information.temperature,
- "temperature_unit": "C",
- "cpu_usage": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": self._get_network_info(),
- "volumes": self._get_volumes_info()
- }
-
- logger.info("Successfully fetched extended system status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system status: {str(e)}")
- return None
-
- def _get_network_info(self) -> List[Dict[str, Any]]:
- """Получение информации о сетевых интерфейсах"""
- try:
- result = []
-
- # Получение информации о сети
- for device in self.dsm.network.interfaces:
- net_info = {
- "device": device,
- "ip": self.dsm.network.get_ip(device),
- "mask": self.dsm.network.get_mask(device),
- "mac": self.dsm.network.get_mac(device),
- "type": self.dsm.network.get_type(device),
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
- result.append(net_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting network info: {str(e)}")
- return []
-
- def _get_volumes_info(self) -> List[Dict[str, Any]]:
- """Получение информации о томах хранения"""
- try:
- result = []
-
- # Получение информации о томах
- for volume in self.dsm.storage.volumes:
- vol_info = {
- "name": volume,
- "status": self.dsm.storage.volume_status(volume),
- "device_type": self.dsm.storage.volume_device_type(volume),
- "total_size": self.dsm.storage.volume_size_total(volume),
- "used_size": self.dsm.storage.volume_size_used(volume),
- "percent_used": self.dsm.storage.volume_percentage_used(volume)
- }
- result.append(vol_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting volumes info: {str(e)}")
- return []
-
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- if not self.dsm and not self.login():
- return []
-
- try:
- result = []
-
- # Получение информации о общих папках
- for folder in self.dsm.share.shares:
- share_info = {
- "name": folder,
- "path": self.dsm.share.get_info(folder).get("path", ""),
- "desc": self.dsm.share.get_info(folder).get("desc", "")
- }
- result.append(share_info)
-
- logger.info(f"Successfully retrieved {len(result)} shared folders")
- return result
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_info(self) -> Dict[str, Any]:
- """Получение основной информации о системе"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "model": self.dsm.information.model,
- "serial": self.dsm.information.serial,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime
- }
-
- logger.info("Successfully fetched system info")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system info: {str(e)}")
- return {}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды выключения
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "shutdown"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system shutdown")
- return True
-
- except Exception as e:
- logger.error(f"Error shutting down system: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды перезагрузки
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "reboot"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system reboot")
- return True
-
- except Exception as e:
- logger.error(f"Error rebooting system: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "cpu_load": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "cached_mb": self.dsm.utilisation.memory_cached_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": {}
- }
-
- # Добавляем данные по сети
- for device in self.dsm.network.interfaces:
- result["network"][device] = {
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
-
- logger.info("Successfully fetched system load")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "volumes": self._get_volumes_info(),
- "disks": self._get_disks_info(),
- "total_size": 0,
- "total_used": 0
- }
-
- # Суммируем общий размер и использование
- for volume in result["volumes"]:
- result["total_size"] += volume["total_size"]
- result["total_used"] += volume["used_size"]
-
- logger.info("Successfully fetched storage status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting storage status: {str(e)}")
- return {}
-
- def _get_disks_info(self) -> List[Dict[str, Any]]:
- """Получение информации о дисках"""
- try:
- result = []
-
- # Получение информации о дисках
- for disk in self.dsm.storage.disks:
- disk_info = {
- "name": disk,
- "model": self.dsm.storage.disk_model(disk),
- "type": self.dsm.storage.disk_type(disk),
- "status": self.dsm.storage.disk_status(disk),
- "temp": self.dsm.storage.disk_temp(disk)
- }
- result.append(disk_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting disks info: {str(e)}")
- return []
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности"""
- if not self.dsm and not self.login():
- return {"success": False}
-
- try:
- # Используем низкоуровневый API для получения информации о безопасности
- endpoint = "SYNO.Core.Security.DSM"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "status"}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response:
- return {
- "success": True,
- "status": response["data"].get("status", "unknown"),
- "last_check": response["data"].get("last_check", None),
- "is_secure": response["data"].get("is_secure", False)
- }
- else:
- return {"success": False}
-
- except Exception as e:
- logger.error(f"Error getting security status: {str(e)}")
- return {"success": False}
-
- def get_users(self) -> List[str]:
- """Получение списка пользователей"""
- if not self.dsm and not self.login():
- return []
-
- try:
- users = []
-
- # Используем низкоуровневый API для получения списка пользователей
- endpoint = "SYNO.Core.User"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "list", "additional": ["email"]}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response and "users" in response["data"]:
- for user in response["data"]["users"]:
- if "name" in user:
- users.append(user["name"])
-
- logger.info(f"Successfully retrieved {len(users)} users")
- return users
-
- except Exception as e:
- logger.error(f"Error getting users: {str(e)}")
- return []
diff --git a/.history/src/api/synology_20250830071755.py b/.history/src/api/synology_20250830071755.py
deleted file mode 100644
index a351e5f..0000000
--- a/.history/src/api/synology_20250830071755.py
+++ /dev/null
@@ -1,446 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List, Tuple
-import socket
-import struct
-from time import sleep
-import urllib3
-from synology_dsm import SynologyDSM
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS с использованием python-synology"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.dsm = None
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS используя python-synology"""
- try:
- # Создаем экземпляр SynologyDSM
- self.dsm = SynologyDSM(
- dsm_ip=SYNOLOGY_HOST,
- dsm_port=SYNOLOGY_PORT,
- username=SYNOLOGY_USERNAME,
- password=SYNOLOGY_PASSWORD,
- use_https=SYNOLOGY_SECURE,
- timeout=SYNOLOGY_TIMEOUT,
- verify_ssl=False
- )
-
- # Авторизация
- self.dsm.login()
- logger.info("Successfully logged in to Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Failed to log in to Synology NAS: {str(e)}")
- self.dsm = None
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.dsm:
- return True
-
- try:
- self.dsm.logout()
- self.dsm = None
- logger.info("Successfully logged out from Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Error during logout: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение расширенного статуса системы"""
- if not self.dsm and not self.login():
- return None
-
- try:
- result = {
- "model": self.dsm.information.model,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime,
- "serial": self.dsm.information.serial,
- "temperature": self.dsm.information.temperature,
- "temperature_unit": "C",
- "cpu_usage": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": self._get_network_info(),
- "volumes": self._get_volumes_info()
- }
-
- logger.info("Successfully fetched extended system status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system status: {str(e)}")
- return None
-
- def _get_network_info(self) -> List[Dict[str, Any]]:
- """Получение информации о сетевых интерфейсах"""
- try:
- result = []
-
- # Получение информации о сети
- for device in self.dsm.network.interfaces:
- net_info = {
- "device": device,
- "ip": self.dsm.network.get_ip(device),
- "mask": self.dsm.network.get_mask(device),
- "mac": self.dsm.network.get_mac(device),
- "type": self.dsm.network.get_type(device),
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
- result.append(net_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting network info: {str(e)}")
- return []
-
- def _get_volumes_info(self) -> List[Dict[str, Any]]:
- """Получение информации о томах хранения"""
- try:
- result = []
-
- # Получение информации о томах
- for volume in self.dsm.storage.volumes:
- vol_info = {
- "name": volume,
- "status": self.dsm.storage.volume_status(volume),
- "device_type": self.dsm.storage.volume_device_type(volume),
- "total_size": self.dsm.storage.volume_size_total(volume),
- "used_size": self.dsm.storage.volume_size_used(volume),
- "percent_used": self.dsm.storage.volume_percentage_used(volume)
- }
- result.append(vol_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting volumes info: {str(e)}")
- return []
-
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- if not self.dsm and not self.login():
- return []
-
- try:
- result = []
-
- # Получение информации о общих папках
- for folder in self.dsm.share.shares:
- share_info = {
- "name": folder,
- "path": self.dsm.share.get_info(folder).get("path", ""),
- "desc": self.dsm.share.get_info(folder).get("desc", "")
- }
- result.append(share_info)
-
- logger.info(f"Successfully retrieved {len(result)} shared folders")
- return result
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_info(self) -> Dict[str, Any]:
- """Получение основной информации о системе"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "model": self.dsm.information.model,
- "serial": self.dsm.information.serial,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime
- }
-
- logger.info("Successfully fetched system info")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system info: {str(e)}")
- return {}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды выключения
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "shutdown"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system shutdown")
- return True
-
- except Exception as e:
- logger.error(f"Error shutting down system: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды перезагрузки
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "reboot"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system reboot")
- return True
-
- except Exception as e:
- logger.error(f"Error rebooting system: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "cpu_load": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "cached_mb": self.dsm.utilisation.memory_cached_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": {}
- }
-
- # Добавляем данные по сети
- for device in self.dsm.network.interfaces:
- result["network"][device] = {
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
-
- logger.info("Successfully fetched system load")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "volumes": self._get_volumes_info(),
- "disks": self._get_disks_info(),
- "total_size": 0,
- "total_used": 0
- }
-
- # Суммируем общий размер и использование
- for volume in result["volumes"]:
- result["total_size"] += volume["total_size"]
- result["total_used"] += volume["used_size"]
-
- logger.info("Successfully fetched storage status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting storage status: {str(e)}")
- return {}
-
- def _get_disks_info(self) -> List[Dict[str, Any]]:
- """Получение информации о дисках"""
- try:
- result = []
-
- # Получение информации о дисках
- for disk in self.dsm.storage.disks:
- disk_info = {
- "name": disk,
- "model": self.dsm.storage.disk_model(disk),
- "type": self.dsm.storage.disk_type(disk),
- "status": self.dsm.storage.disk_status(disk),
- "temp": self.dsm.storage.disk_temp(disk)
- }
- result.append(disk_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting disks info: {str(e)}")
- return []
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности"""
- if not self.dsm and not self.login():
- return {"success": False}
-
- try:
- # Используем низкоуровневый API для получения информации о безопасности
- endpoint = "SYNO.Core.Security.DSM"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "status"}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response:
- return {
- "success": True,
- "status": response["data"].get("status", "unknown"),
- "last_check": response["data"].get("last_check", None),
- "is_secure": response["data"].get("is_secure", False)
- }
- else:
- return {"success": False}
-
- except Exception as e:
- logger.error(f"Error getting security status: {str(e)}")
- return {"success": False}
-
- def get_users(self) -> List[str]:
- """Получение списка пользователей"""
- if not self.dsm and not self.login():
- return []
-
- try:
- users = []
-
- # Используем низкоуровневый API для получения списка пользователей
- endpoint = "SYNO.Core.User"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "list", "additional": ["email"]}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response and "users" in response["data"]:
- for user in response["data"]["users"]:
- if "name" in user:
- users.append(user["name"])
-
- logger.info(f"Successfully retrieved {len(users)} users")
- return users
-
- except Exception as e:
- logger.error(f"Error getting users: {str(e)}")
- return []
diff --git a/.history/src/api/synology_20250830071934.py b/.history/src/api/synology_20250830071934.py
deleted file mode 100644
index 5a89749..0000000
--- a/.history/src/api/synology_20250830071934.py
+++ /dev/null
@@ -1,445 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS с использованием python-synology"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.dsm = None
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS используя python-synology"""
- try:
- # Создаем экземпляр SynologyDSM
- self.dsm = SynologyDSM(
- dsm_ip=SYNOLOGY_HOST,
- dsm_port=SYNOLOGY_PORT,
- username=SYNOLOGY_USERNAME,
- password=SYNOLOGY_PASSWORD,
- use_https=SYNOLOGY_SECURE,
- timeout=SYNOLOGY_TIMEOUT,
- verify_ssl=False
- )
-
- # Авторизация
- self.dsm.login()
- logger.info("Successfully logged in to Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Failed to log in to Synology NAS: {str(e)}")
- self.dsm = None
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.dsm:
- return True
-
- try:
- self.dsm.logout()
- self.dsm = None
- logger.info("Successfully logged out from Synology NAS")
- return True
-
- except Exception as e:
- logger.error(f"Error during logout: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение расширенного статуса системы"""
- if not self.dsm and not self.login():
- return None
-
- try:
- result = {
- "model": self.dsm.information.model,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime,
- "serial": self.dsm.information.serial,
- "temperature": self.dsm.information.temperature,
- "temperature_unit": "C",
- "cpu_usage": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": self._get_network_info(),
- "volumes": self._get_volumes_info()
- }
-
- logger.info("Successfully fetched extended system status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system status: {str(e)}")
- return None
-
- def _get_network_info(self) -> List[Dict[str, Any]]:
- """Получение информации о сетевых интерфейсах"""
- try:
- result = []
-
- # Получение информации о сети
- for device in self.dsm.network.interfaces:
- net_info = {
- "device": device,
- "ip": self.dsm.network.get_ip(device),
- "mask": self.dsm.network.get_mask(device),
- "mac": self.dsm.network.get_mac(device),
- "type": self.dsm.network.get_type(device),
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
- result.append(net_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting network info: {str(e)}")
- return []
-
- def _get_volumes_info(self) -> List[Dict[str, Any]]:
- """Получение информации о томах хранения"""
- try:
- result = []
-
- # Получение информации о томах
- for volume in self.dsm.storage.volumes:
- vol_info = {
- "name": volume,
- "status": self.dsm.storage.volume_status(volume),
- "device_type": self.dsm.storage.volume_device_type(volume),
- "total_size": self.dsm.storage.volume_size_total(volume),
- "used_size": self.dsm.storage.volume_size_used(volume),
- "percent_used": self.dsm.storage.volume_percentage_used(volume)
- }
- result.append(vol_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting volumes info: {str(e)}")
- return []
-
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- if not self.dsm and not self.login():
- return []
-
- try:
- result = []
-
- # Получение информации о общих папках
- for folder in self.dsm.share.shares:
- share_info = {
- "name": folder,
- "path": self.dsm.share.get_info(folder).get("path", ""),
- "desc": self.dsm.share.get_info(folder).get("desc", "")
- }
- result.append(share_info)
-
- logger.info(f"Successfully retrieved {len(result)} shared folders")
- return result
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_info(self) -> Dict[str, Any]:
- """Получение основной информации о системе"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "model": self.dsm.information.model,
- "serial": self.dsm.information.serial,
- "version": self.dsm.information.version_string,
- "uptime": self.dsm.information.uptime
- }
-
- logger.info("Successfully fetched system info")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system info: {str(e)}")
- return {}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды выключения
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "shutdown"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system shutdown")
- return True
-
- except Exception as e:
- logger.error(f"Error shutting down system: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.dsm and not self.login():
- return False
-
- try:
- # Используем низкоуровневый API для отправки команды перезагрузки
- endpoint = "SYNO.DSM.System"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "reboot"}
-
- self.dsm.post(endpoint, api_path, req_param)
- logger.info("Successfully initiated system reboot")
- return True
-
- except Exception as e:
- logger.error(f"Error rebooting system: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "cpu_load": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "cached_mb": self.dsm.utilisation.memory_cached_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": {}
- }
-
- # Добавляем данные по сети
- for device in self.dsm.network.interfaces:
- result["network"][device] = {
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
-
- logger.info("Successfully fetched system load")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "volumes": self._get_volumes_info(),
- "disks": self._get_disks_info(),
- "total_size": 0,
- "total_used": 0
- }
-
- # Суммируем общий размер и использование
- for volume in result["volumes"]:
- result["total_size"] += volume["total_size"]
- result["total_used"] += volume["used_size"]
-
- logger.info("Successfully fetched storage status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting storage status: {str(e)}")
- return {}
-
- def _get_disks_info(self) -> List[Dict[str, Any]]:
- """Получение информации о дисках"""
- try:
- result = []
-
- # Получение информации о дисках
- for disk in self.dsm.storage.disks:
- disk_info = {
- "name": disk,
- "model": self.dsm.storage.disk_model(disk),
- "type": self.dsm.storage.disk_type(disk),
- "status": self.dsm.storage.disk_status(disk),
- "temp": self.dsm.storage.disk_temp(disk)
- }
- result.append(disk_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting disks info: {str(e)}")
- return []
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности"""
- if not self.dsm and not self.login():
- return {"success": False}
-
- try:
- # Используем низкоуровневый API для получения информации о безопасности
- endpoint = "SYNO.Core.Security.DSM"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "status"}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response:
- return {
- "success": True,
- "status": response["data"].get("status", "unknown"),
- "last_check": response["data"].get("last_check", None),
- "is_secure": response["data"].get("is_secure", False)
- }
- else:
- return {"success": False}
-
- except Exception as e:
- logger.error(f"Error getting security status: {str(e)}")
- return {"success": False}
-
- def get_users(self) -> List[str]:
- """Получение списка пользователей"""
- if not self.dsm and not self.login():
- return []
-
- try:
- users = []
-
- # Используем низкоуровневый API для получения списка пользователей
- endpoint = "SYNO.Core.User"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "list", "additional": ["email"]}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response and "users" in response["data"]:
- for user in response["data"]["users"]:
- if "name" in user:
- users.append(user["name"])
-
- logger.info(f"Successfully retrieved {len(users)} users")
- return users
-
- except Exception as e:
- logger.error(f"Error getting users: {str(e)}")
- return []
diff --git a/.history/src/api/synology_20250830072031.py b/.history/src/api/synology_20250830072031.py
deleted file mode 100644
index 1468e04..0000000
--- a/.history/src/api/synology_20250830072031.py
+++ /dev/null
@@ -1,398 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- if not self.sid and not self.login():
- return None
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data")
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
- return None
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return None
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "cpu_load": self.dsm.utilisation.cpu_total_load,
- "memory": {
- "total_mb": self.dsm.utilisation.memory_size_mb,
- "available_mb": self.dsm.utilisation.memory_available_real_mb,
- "cached_mb": self.dsm.utilisation.memory_cached_mb,
- "usage_percent": self.dsm.utilisation.memory_real_usage
- },
- "network": {}
- }
-
- # Добавляем данные по сети
- for device in self.dsm.network.interfaces:
- result["network"][device] = {
- "rx_bytes": self.dsm.network.get_rx(device),
- "tx_bytes": self.dsm.network.get_tx(device)
- }
-
- logger.info("Successfully fetched system load")
- return result
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- if not self.dsm and not self.login():
- return {}
-
- try:
- result = {
- "volumes": self._get_volumes_info(),
- "disks": self._get_disks_info(),
- "total_size": 0,
- "total_used": 0
- }
-
- # Суммируем общий размер и использование
- for volume in result["volumes"]:
- result["total_size"] += volume["total_size"]
- result["total_used"] += volume["used_size"]
-
- logger.info("Successfully fetched storage status")
- return result
-
- except Exception as e:
- logger.error(f"Error getting storage status: {str(e)}")
- return {}
-
- def _get_disks_info(self) -> List[Dict[str, Any]]:
- """Получение информации о дисках"""
- try:
- result = []
-
- # Получение информации о дисках
- for disk in self.dsm.storage.disks:
- disk_info = {
- "name": disk,
- "model": self.dsm.storage.disk_model(disk),
- "type": self.dsm.storage.disk_type(disk),
- "status": self.dsm.storage.disk_status(disk),
- "temp": self.dsm.storage.disk_temp(disk)
- }
- result.append(disk_info)
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting disks info: {str(e)}")
- return []
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности"""
- if not self.dsm and not self.login():
- return {"success": False}
-
- try:
- # Используем низкоуровневый API для получения информации о безопасности
- endpoint = "SYNO.Core.Security.DSM"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "status"}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response:
- return {
- "success": True,
- "status": response["data"].get("status", "unknown"),
- "last_check": response["data"].get("last_check", None),
- "is_secure": response["data"].get("is_secure", False)
- }
- else:
- return {"success": False}
-
- except Exception as e:
- logger.error(f"Error getting security status: {str(e)}")
- return {"success": False}
-
- def get_users(self) -> List[str]:
- """Получение списка пользователей"""
- if not self.dsm and not self.login():
- return []
-
- try:
- users = []
-
- # Используем низкоуровневый API для получения списка пользователей
- endpoint = "SYNO.Core.User"
- api_path = "entry.cgi"
- req_param = {"version": 1, "method": "list", "additional": ["email"]}
-
- response = self.dsm.get(endpoint, api_path, req_param)
-
- if response and "data" in response and "users" in response["data"]:
- for user in response["data"]["users"]:
- if "name" in user:
- users.append(user["name"])
-
- logger.info(f"Successfully retrieved {len(users)} users")
- return users
-
- except Exception as e:
- logger.error(f"Error getting users: {str(e)}")
- return []
diff --git a/.history/src/api/synology_20250830072122.py b/.history/src/api/synology_20250830072122.py
deleted file mode 100644
index 2344021..0000000
--- a/.history/src/api/synology_20250830072122.py
+++ /dev/null
@@ -1,287 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- if not self.sid and not self.login():
- return None
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data")
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
- return None
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return None
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
- return {}
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
- return {"success": False}
diff --git a/.history/src/api/synology_20250830072817.py b/.history/src/api/synology_20250830072817.py
deleted file mode 100644
index 2344021..0000000
--- a/.history/src/api/synology_20250830072817.py
+++ /dev/null
@@ -1,287 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- if not self.sid and not self.login():
- return None
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data")
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
- return None
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return None
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
- return {}
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
- return {"success": False}
diff --git a/.history/src/api/synology_20250830073007.py b/.history/src/api/synology_20250830073007.py
deleted file mode 100644
index f39c05a..0000000
--- a/.history/src/api/synology_20250830073007.py
+++ /dev/null
@@ -1,309 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- return self.get_system_status()
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
- return {}
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
- return {"success": False}
diff --git a/.history/src/api/synology_20250830073043.py b/.history/src/api/synology_20250830073043.py
deleted file mode 100644
index f39c05a..0000000
--- a/.history/src/api/synology_20250830073043.py
+++ /dev/null
@@ -1,309 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- return self.get_system_status()
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
- return {}
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
- return {"success": False}
diff --git a/.history/src/api/synology_20250830073131.py b/.history/src/api/synology_20250830073131.py
deleted file mode 100644
index ca41c27..0000000
--- a/.history/src/api/synology_20250830073131.py
+++ /dev/null
@@ -1,326 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "_sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
- return {}
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
- return {"success": False}
diff --git a/.history/src/api/synology_20250830073142.py b/.history/src/api/synology_20250830073142.py
deleted file mode 100644
index a2e3a8a..0000000
--- a/.history/src/api/synology_20250830073142.py
+++ /dev/null
@@ -1,326 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "_sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
- return {}
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
- return {"success": False}
diff --git a/.history/src/api/synology_20250830073153.py b/.history/src/api/synology_20250830073153.py
deleted file mode 100644
index 8fd4251..0000000
--- a/.history/src/api/synology_20250830073153.py
+++ /dev/null
@@ -1,326 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
- return {}
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
- return {"success": False}
diff --git a/.history/src/api/synology_20250830073204.py b/.history/src/api/synology_20250830073204.py
deleted file mode 100644
index 75e5505..0000000
--- a/.history/src/api/synology_20250830073204.py
+++ /dev/null
@@ -1,326 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
- return {}
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
- return {"success": False}
diff --git a/.history/src/api/synology_20250830073217.py b/.history/src/api/synology_20250830073217.py
deleted file mode 100644
index 75e5505..0000000
--- a/.history/src/api/synology_20250830073217.py
+++ /dev/null
@@ -1,326 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
- return {}
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
- return {"success": False}
diff --git a/.history/src/api/synology_20250830073544.py b/.history/src/api/synology_20250830073544.py
deleted file mode 100644
index 08e1daf..0000000
--- a/.history/src/api/synology_20250830073544.py
+++ /dev/null
@@ -1,353 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering shutdown successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
- return {}
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
- return {"success": False}
diff --git a/.history/src/api/synology_20250830073606.py b/.history/src/api/synology_20250830073606.py
deleted file mode 100644
index d4755b1..0000000
--- a/.history/src/api/synology_20250830073606.py
+++ /dev/null
@@ -1,380 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering shutdown successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
- return {}
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
- return {"success": False}
diff --git a/.history/src/api/synology_20250830073620.py b/.history/src/api/synology_20250830073620.py
deleted file mode 100644
index d4755b1..0000000
--- a/.history/src/api/synology_20250830073620.py
+++ /dev/null
@@ -1,380 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering shutdown successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
- return {}
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
- return {"success": False}
diff --git a/.history/src/api/synology_20250830073939.py b/.history/src/api/synology_20250830073939.py
deleted file mode 100644
index ebada57..0000000
--- a/.history/src/api/synology_20250830073939.py
+++ /dev/null
@@ -1,432 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering shutdown successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, bool]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830073954.py b/.history/src/api/synology_20250830073954.py
deleted file mode 100644
index 9add3ef..0000000
--- a/.history/src/api/synology_20250830073954.py
+++ /dev/null
@@ -1,432 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering shutdown successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074025.py b/.history/src/api/synology_20250830074025.py
deleted file mode 100644
index 9500572..0000000
--- a/.history/src/api/synology_20250830074025.py
+++ /dev/null
@@ -1,458 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074140.py b/.history/src/api/synology_20250830074140.py
deleted file mode 100644
index 9500572..0000000
--- a/.history/src/api/synology_20250830074140.py
+++ /dev/null
@@ -1,458 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074228.py b/.history/src/api/synology_20250830074228.py
deleted file mode 100644
index ec41f0a..0000000
--- a/.history/src/api/synology_20250830074228.py
+++ /dev/null
@@ -1,479 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-import json
-import logging
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-import urllib3
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = urllib3.util.Retry(
- total=3,
- status_forcelist=[429, 500, 502, 503, 504],
- allowed_methods=["GET", "POST"],
- backoff_factor=1
- )
- adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy)
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
-
- # Время последней успешной аутентификации
- self._last_auth_time = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074245.py b/.history/src/api/synology_20250830074245.py
deleted file mode 100644
index 397a969..0000000
--- a/.history/src/api/synology_20250830074245.py
+++ /dev/null
@@ -1,482 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = urllib3.util.Retry(
- total=3,
- status_forcelist=[429, 500, 502, 503, 504],
- allowed_methods=["GET", "POST"],
- backoff_factor=1
- )
- adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy)
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
-
- # Время последней успешной аутентификации
- self._last_auth_time = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074313.py b/.history/src/api/synology_20250830074313.py
deleted file mode 100644
index 4d49f83..0000000
--- a/.history/src/api/synology_20250830074313.py
+++ /dev/null
@@ -1,482 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=3,
- status_forcelist=[429, 500, 502, 503, 504],
- allowed_methods=["GET", "POST"],
- backoff_factor=1
- )
- adapter = HTTPAdapter(max_retries=retry_strategy)
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
-
- # Время последней успешной аутентификации
- self._last_auth_time = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074442.py b/.history/src/api/synology_20250830074442.py
deleted file mode 100644
index 4d49f83..0000000
--- a/.history/src/api/synology_20250830074442.py
+++ /dev/null
@@ -1,482 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=3,
- status_forcelist=[429, 500, 502, 503, 504],
- allowed_methods=["GET", "POST"],
- backoff_factor=1
- )
- adapter = HTTPAdapter(max_retries=retry_strategy)
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
-
- # Время последней успешной аутентификации
- self._last_auth_time = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074627.py b/.history/src/api/synology_20250830074627.py
deleted file mode 100644
index ac0d839..0000000
--- a/.history/src/api/synology_20250830074627.py
+++ /dev/null
@@ -1,499 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Установка таймаутов для сессии (подключение, чтение)
- self.session.timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074636.py b/.history/src/api/synology_20250830074636.py
deleted file mode 100644
index 451b4fc..0000000
--- a/.history/src/api/synology_20250830074636.py
+++ /dev/null
@@ -1,500 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- logger.info("Successfully logged in to Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074708.py b/.history/src/api/synology_20250830074708.py
deleted file mode 100644
index 6a6f72c..0000000
--- a/.history/src/api/synology_20250830074708.py
+++ /dev/null
@@ -1,562 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом
- online_status = self.is_online(force_check=True)
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code} - {error_desc}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074721.py b/.history/src/api/synology_20250830074721.py
deleted file mode 100644
index 66a4c33..0000000
--- a/.history/src/api/synology_20250830074721.py
+++ /dev/null
@@ -1,562 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом
- online_status = self.is_online()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code} - {error_desc}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074730.py b/.history/src/api/synology_20250830074730.py
deleted file mode 100644
index 01727c4..0000000
--- a/.history/src/api/synology_20250830074730.py
+++ /dev/null
@@ -1,561 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом
- online_status = self.is_online()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def is_online(self) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error:
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074755.py b/.history/src/api/synology_20250830074755.py
deleted file mode 100644
index 1967301..0000000
--- a/.history/src/api/synology_20250830074755.py
+++ /dev/null
@@ -1,637 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом
- online_status = self.is_online()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def get_system_status(self) -> Optional[Dict[str, Any]]:
- """Получение статуса системы"""
- # Если устройство недоступно, сразу возвращаем минимальную информацию
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- if not self.sid and not self.login():
- logger.warning("Not authenticated, returning minimal status")
- return {"status": "unknown", "error": "authentication_failed"}
-
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid # Проверяем правильность параметра sid вместо _sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully fetched system status")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to get system status: Error code {error_code}")
-
- # Ошибка 104 - требуется авторизация или неверное API
- if error_code == 104:
- # Пробуем переавторизоваться
- if self.login():
- logger.info("Re-authenticated, trying again")
- # Пробуем получить информацию еще раз, но без рекурсивного вызова
- try:
- url = f"{self.base_url}/entry.cgi"
- retry_params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- retry_data = retry_response.json()
-
- if retry_data.get("success"):
- logger.info("Successfully fetched system status after re-authentication")
- return retry_data.get("data", {})
- except Exception as e:
- logger.error(f"Error during retry: {str(e)}")
-
- # Возвращаем минимальную информацию с ошибкой
- return {
- "status": "error",
- "error_code": error_code,
- "is_online": True
- }
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
- except Exception as e:
- logger.error(f"Unexpected error getting status: {str(e)}")
- return {"status": "error", "error": str(e), "is_online": True}
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
- try:
- # Сначала проверяем TCP-соединение
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- online_status = (result == 0)
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно, попробуем более детальную проверку через API
- if online_status:
- logger.info("Trying to fetch more detailed online status through API...")
- # Пробуем получить информацию, но не вызываем is_online() рекурсивно
- if self.sid or self.login():
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
- except socket.error as e:
- logger.error(f"Socket error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
- except Exception as e:
- logger.error(f"Unexpected error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074838.py b/.history/src/api/synology_20250830074838.py
deleted file mode 100644
index 4558a9a..0000000
--- a/.history/src/api/synology_20250830074838.py
+++ /dev/null
@@ -1,686 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом
- online_status = self.is_online()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Dict[str, Any] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
- try:
- # Сначала проверяем TCP-соединение
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- online_status = (result == 0)
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно, попробуем более детальную проверку через API
- if online_status:
- logger.info("Trying to fetch more detailed online status through API...")
- # Пробуем получить информацию, но не вызываем is_online() рекурсивно
- if self.sid or self.login():
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
- except socket.error as e:
- logger.error(f"Socket error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
- except Exception as e:
- logger.error(f"Unexpected error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074850.py b/.history/src/api/synology_20250830074850.py
deleted file mode 100644
index 0f2ebda..0000000
--- a/.history/src/api/synology_20250830074850.py
+++ /dev/null
@@ -1,686 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом
- online_status = self.is_online()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online():
- logger.info("Device is already offline, no need to shut down")
- return True
-
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shutdown")
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First shutdown attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "shutdown",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to shutdown system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её выключить
- if error_code == 102:
- logger.info("System returned permission error. This is common when using user without proper rights.")
- logger.info("The shutdown command might still be processed, checking status...")
-
- # Даем системе несколько секунд, чтобы начать выключение
- sleep(5)
-
- # Проверяем, стала ли система недоступна
- if not self.is_online():
- logger.info("System is now offline. Shutdown appears successful.")
- return True
- else:
- logger.info("System is still online. Shutdown may have failed.")
-
- return False
- except Exception as e:
- logger.error(f"Second shutdown attempt failed: {str(e)}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
-
- # Если после попытки выключения соединение потеряно, возможно, выключение успешно
- if not self.is_online():
- logger.info("Connection lost after shutdown request, device appears to be shutting down")
- return True
-
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
- try:
- # Сначала проверяем TCP-соединение
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- online_status = (result == 0)
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно, попробуем более детальную проверку через API
- if online_status:
- logger.info("Trying to fetch more detailed online status through API...")
- # Пробуем получить информацию, но не вызываем is_online() рекурсивно
- if self.sid or self.login():
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
- except socket.error as e:
- logger.error(f"Socket error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
- except Exception as e:
- logger.error(f"Unexpected error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074920.py b/.history/src/api/synology_20250830074920.py
deleted file mode 100644
index 93d75da..0000000
--- a/.history/src/api/synology_20250830074920.py
+++ /dev/null
@@ -1,656 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом
- online_status = self.is_online()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "shutdown")
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- if not self.sid and not self.login():
- return False
-
- try:
- # Пробуем DSM.System API
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.System",
- "version": "1",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
- return True
- except Exception as e:
- logger.warning(f"First reboot attempt failed: {str(e)}")
-
- # Пробуем альтернативный API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.System",
- "version": "3",
- "method": "reboot",
- "sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to reboot system: Error code {error_code}")
-
- # Если система недоступна по API, но доступна по сети,
- # считаем, что мы все равно можем её перезагрузить
- if error_code == 102 and self.is_online():
- logger.info("System is online but API returns permission error. Considering reboot successful anyway.")
- return True
-
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
- try:
- # Сначала проверяем TCP-соединение
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- online_status = (result == 0)
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно, попробуем более детальную проверку через API
- if online_status:
- logger.info("Trying to fetch more detailed online status through API...")
- # Пробуем получить информацию, но не вызываем is_online() рекурсивно
- if self.sid or self.login():
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
- except socket.error as e:
- logger.error(f"Socket error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
- except Exception as e:
- logger.error(f"Unexpected error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830074947.py b/.history/src/api/synology_20250830074947.py
deleted file mode 100644
index b9686c0..0000000
--- a/.history/src/api/synology_20250830074947.py
+++ /dev/null
@@ -1,669 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом
- online_status = self.is_online()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "shutdown")
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "reboot")
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
- try:
- # Сначала проверяем TCP-соединение
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- online_status = (result == 0)
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно, попробуем более детальную проверку через API
- if online_status:
- logger.info("Trying to fetch more detailed online status through API...")
- # Пробуем получить информацию, но не вызываем is_online() рекурсивно
- if self.sid or self.login():
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
- except socket.error as e:
- logger.error(f"Socket error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
- except Exception as e:
- logger.error(f"Unexpected error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- if self.is_online():
- logger.info("Synology NAS is already online")
- return True
-
- # Отправка WoL пакета
- if not self.wake_on_lan():
- return False
-
- # Ожидание загрузки
- return self.wait_for_boot()
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online():
- logger.info("Synology NAS is already offline")
- return True
-
- return self.shutdown_system()
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830075007.py b/.history/src/api/synology_20250830075007.py
deleted file mode 100644
index a6d0077..0000000
--- a/.history/src/api/synology_20250830075007.py
+++ /dev/null
@@ -1,717 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом
- online_status = self.is_online()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "shutdown")
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "reboot")
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
- try:
- # Сначала проверяем TCP-соединение
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- online_status = (result == 0)
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно, попробуем более детальную проверку через API
- if online_status:
- logger.info("Trying to fetch more detailed online status through API...")
- # Пробуем получить информацию, но не вызываем is_online() рекурсивно
- if self.sid or self.login():
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
- except socket.error as e:
- logger.error(f"Socket error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
- except Exception as e:
- logger.error(f"Unexpected error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- mac_bytes = bytes.fromhex(mac_address)
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
-
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}")
- return True
-
- except Exception as e:
- logger.error(f"Error sending Wake-on-LAN packet: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info("Waiting for Synology NAS to boot...")
-
- for attempt in range(max_attempts):
- if self.is_online():
- logger.info(f"Synology NAS is online after {attempt + 1} attempts")
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts} attempts")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830075041.py b/.history/src/api/synology_20250830075041.py
deleted file mode 100644
index 1b31283..0000000
--- a/.history/src/api/synology_20250830075041.py
+++ /dev/null
@@ -1,762 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом
- online_status = self.is_online()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "shutdown")
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "reboot")
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
- try:
- # Сначала проверяем TCP-соединение
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- online_status = (result == 0)
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно, попробуем более детальную проверку через API
- if online_status:
- logger.info("Trying to fetch more detailed online status through API...")
- # Пробуем получить информацию, но не вызываем is_online() рекурсивно
- if self.sid or self.login():
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
- except socket.error as e:
- logger.error(f"Socket error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
- except Exception as e:
- logger.error(f"Unexpected error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Получаем широковещательный адрес для локальной сети
- # Предполагаем, что адрес завершается на .255
- broadcast_addr = SYNOLOGY_HOST.rsplit('.', 1)[0] + '.255'
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830075053.py b/.history/src/api/synology_20250830075053.py
deleted file mode 100644
index 1149fa0..0000000
--- a/.history/src/api/synology_20250830075053.py
+++ /dev/null
@@ -1,761 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом
- online_status = self.is_online()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "shutdown")
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "reboot")
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
- try:
- # Сначала проверяем TCP-соединение
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- online_status = (result == 0)
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно, попробуем более детальную проверку через API
- if online_status:
- logger.info("Trying to fetch more detailed online status through API...")
- # Пробуем получить информацию, но не вызываем is_online() рекурсивно
- if self.sid or self.login():
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
- except socket.error as e:
- logger.error(f"Socket error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
- except Exception as e:
- logger.error(f"Unexpected error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830075107.py b/.history/src/api/synology_20250830075107.py
deleted file mode 100644
index 1149fa0..0000000
--- a/.history/src/api/synology_20250830075107.py
+++ /dev/null
@@ -1,761 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом
- online_status = self.is_online()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "shutdown")
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "reboot")
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
- try:
- # Сначала проверяем TCP-соединение
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- online_status = (result == 0)
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно, попробуем более детальную проверку через API
- if online_status:
- logger.info("Trying to fetch more detailed online status through API...")
- # Пробуем получить информацию, но не вызываем is_online() рекурсивно
- if self.sid or self.login():
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
- except socket.error as e:
- logger.error(f"Socket error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
- except Exception as e:
- logger.error(f"Unexpected error during online check: {str(e)}")
- self._last_online_check = current_time
- self._last_online_status = False
- return False
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830075248.py b/.history/src/api/synology_20250830075248.py
deleted file mode 100644
index 95ecda7..0000000
--- a/.history/src/api/synology_20250830075248.py
+++ /dev/null
@@ -1,761 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом
- online_status = self.is_online()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "shutdown")
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "reboot")
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830075326.py b/.history/src/api/synology_20250830075326.py
deleted file mode 100644
index 4337cd4..0000000
--- a/.history/src/api/synology_20250830075326.py
+++ /dev/null
@@ -1,762 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "shutdown")
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "reboot")
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830075348.py b/.history/src/api/synology_20250830075348.py
deleted file mode 100644
index 4337cd4..0000000
--- a/.history/src/api/synology_20250830075348.py
+++ /dev/null
@@ -1,762 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "shutdown")
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "reboot")
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830075503.py b/.history/src/api/synology_20250830075503.py
deleted file mode 100644
index 5e2e14f..0000000
--- a/.history/src/api/synology_20250830075503.py
+++ /dev/null
@@ -1,770 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "shutdown")
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "reboot")
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830075522.py b/.history/src/api/synology_20250830075522.py
deleted file mode 100644
index 5e2e14f..0000000
--- a/.history/src/api/synology_20250830075522.py
+++ /dev/null
@@ -1,770 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "shutdown")
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "reboot")
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.DSM.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830080635.py b/.history/src/api/synology_20250830080635.py
deleted file mode 100644
index 391ab86..0000000
--- a/.history/src/api/synology_20250830080635.py
+++ /dev/null
@@ -1,769 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Пробуем DSM.System API (первый метод)
- result = self._make_api_request("SYNO.DSM.System", "shutdown")
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Пробуем альтернативный Core.System API (второй метод)
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Правильный API-вызов для перезагрузки согласно официальной документации DSM API
- result = self._make_api_request("SYNO.Core.System.Power", "restart", version=1)
-
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System.Power")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- # Запасной вариант, если первый метод не сработал
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830080658.py b/.history/src/api/synology_20250830080658.py
deleted file mode 100644
index ea1d544..0000000
--- a/.history/src/api/synology_20250830080658.py
+++ /dev/null
@@ -1,769 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "3",
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- logger.debug(f"Sending auth request to {url}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Правильный API-вызов для выключения согласно официальной документации DSM API
- result = self._make_api_request("SYNO.Core.System.Power", "shutdown", version=1)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System.Power")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Запасной вариант с устаревшим API
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Правильный API-вызов для перезагрузки согласно официальной документации DSM API
- result = self._make_api_request("SYNO.Core.System.Power", "restart", version=1)
-
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System.Power")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- # Запасной вариант, если первый метод не сработал
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830080742.py b/.history/src/api/synology_20250830080742.py
deleted file mode 100644
index 1b4b472..0000000
--- a/.history/src/api/synology_20250830080742.py
+++ /dev/null
@@ -1,819 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/entry.cgi"
- logger.debug(f"API request: {api_name}.{method} v{version}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"API error for {api_name}.{method}: {error_code}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Правильный API-вызов для выключения согласно официальной документации DSM API
- result = self._make_api_request("SYNO.Core.System.Power", "shutdown", version=1)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System.Power")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Запасной вариант с устаревшим API
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Правильный API-вызов для перезагрузки согласно официальной документации DSM API
- result = self._make_api_request("SYNO.Core.System.Power", "restart", version=1)
-
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System.Power")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- # Запасной вариант, если первый метод не сработал
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830080825.py b/.history/src/api/synology_20250830080825.py
deleted file mode 100644
index 416f33a..0000000
--- a/.history/src/api/synology_20250830080825.py
+++ /dev/null
@@ -1,866 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Правильный API-вызов для выключения согласно официальной документации DSM API
- result = self._make_api_request("SYNO.Core.System.Power", "shutdown", version=1)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System.Power")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Запасной вариант с устаревшим API
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Правильный API-вызов для перезагрузки согласно официальной документации DSM API
- result = self._make_api_request("SYNO.Core.System.Power", "restart", version=1)
-
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System.Power")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- # Запасной вариант, если первый метод не сработал
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830080858.py b/.history/src/api/synology_20250830080858.py
deleted file mode 100644
index 416f33a..0000000
--- a/.history/src/api/synology_20250830080858.py
+++ /dev/null
@@ -1,866 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Правильный API-вызов для выключения согласно официальной документации DSM API
- result = self._make_api_request("SYNO.Core.System.Power", "shutdown", version=1)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System.Power")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Запасной вариант с устаревшим API
- logger.info("First shutdown method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "shutdown", version=3)
- if result is not None:
- logger.info("Successfully initiated system shutdown using SYNO.Core.System")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
-
- # Если оба метода не сработали, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Правильный API-вызов для перезагрузки согласно официальной документации DSM API
- result = self._make_api_request("SYNO.Core.System.Power", "restart", version=1)
-
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System.Power")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- # Запасной вариант, если первый метод не сработал
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830081426.py b/.history/src/api/synology_20250830081426.py
deleted file mode 100644
index d6e595b..0000000
--- a/.history/src/api/synology_20250830081426.py
+++ /dev/null
@@ -1,910 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Проверка всех доступных методов API для выключения
- # Проверяем наличие API перед использованием
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Правильный API-вызов для перезагрузки согласно официальной документации DSM API
- result = self._make_api_request("SYNO.Core.System.Power", "restart", version=1)
-
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System.Power")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- # Запасной вариант, если первый метод не сработал
- logger.info("First reboot method failed, trying alternative API...")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=3)
- if result is not None:
- logger.info("Successfully initiated system reboot using SYNO.Core.System")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command")
- return False
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830081505.py b/.history/src/api/synology_20250830081505.py
deleted file mode 100644
index 1e92c48..0000000
--- a/.history/src/api/synology_20250830081505.py
+++ /dev/null
@@ -1,943 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Делаем API запрос
- result = self._make_api_request("SYNO.DSM.Info", "getinfo")
-
- if result:
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
- else:
- # Если запрос не удался, но система онлайн
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Проверка всех доступных методов API для выключения
- # Проверяем наличие API перед использованием
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830081538.py b/.history/src/api/synology_20250830081538.py
deleted file mode 100644
index 8b91197..0000000
--- a/.history/src/api/synology_20250830081538.py
+++ /dev/null
@@ -1,956 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Пробуем разные API для получения информации о системе
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Проверка всех доступных методов API для выключения
- # Проверяем наличие API перед использованием
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830081615.py b/.history/src/api/synology_20250830081615.py
deleted file mode 100644
index 8b91197..0000000
--- a/.history/src/api/synology_20250830081615.py
+++ /dev/null
@@ -1,956 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Пробуем разные API для получения информации о системе
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Проверка всех доступных методов API для выключения
- # Проверяем наличие API перед использованием
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.DSM.Info",
- "version": "1",
- "method": "getinfo",
- "sid": self.sid
- }
-
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info("API request successful for getinfo")
- logger.info("Synology NAS is online with API access")
- else:
- logger.warning("API response indicates an error, but NAS is reachable")
- logger.debug(f"API error: {data.get('error', {}).get('code', -1)}")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830081654.py b/.history/src/api/synology_20250830081654.py
deleted file mode 100644
index cc303c7..0000000
--- a/.history/src/api/synology_20250830081654.py
+++ /dev/null
@@ -1,972 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Пробуем разные API для получения информации о системе
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Проверка всех доступных методов API для выключения
- # Проверяем наличие API перед использованием
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830081744.py b/.history/src/api/synology_20250830081744.py
deleted file mode 100644
index d4ce873..0000000
--- a/.history/src/api/synology_20250830081744.py
+++ /dev/null
@@ -1,976 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Пробуем разные API для получения информации о системе
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Проверка всех доступных методов API для выключения
- # Проверяем наличие API перед использованием
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830081837.py b/.history/src/api/synology_20250830081837.py
deleted file mode 100644
index 7b9eff7..0000000
--- a/.history/src/api/synology_20250830081837.py
+++ /dev/null
@@ -1,978 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Пробуем разные API для получения информации о системе
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Проверка всех доступных методов API для выключения
- # Проверяем наличие API перед использованием
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830081856.py b/.history/src/api/synology_20250830081856.py
deleted file mode 100644
index 4d9457d..0000000
--- a/.history/src/api/synology_20250830081856.py
+++ /dev/null
@@ -1,977 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Пробуем разные API для получения информации о системе
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Проверка всех доступных методов API для выключения
- # Проверяем наличие API перед использованием
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830081957.py b/.history/src/api/synology_20250830081957.py
deleted file mode 100644
index 4d9457d..0000000
--- a/.history/src/api/synology_20250830081957.py
+++ /dev/null
@@ -1,977 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Пробуем разные API для получения информации о системе
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Проверка всех доступных методов API для выключения
- # Проверяем наличие API перед использованием
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830082235.py b/.history/src/api/synology_20250830082235.py
deleted file mode 100644
index d75371d..0000000
--- a/.history/src/api/synology_20250830082235.py
+++ /dev/null
@@ -1,980 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Пробуем разные API для получения информации о системе
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Проверка всех доступных методов API для выключения
- # Проверяем наличие API перед использованием
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830082307.py b/.history/src/api/synology_20250830082307.py
deleted file mode 100644
index 0d5a4b2..0000000
--- a/.history/src/api/synology_20250830082307.py
+++ /dev/null
@@ -1,1018 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Проверка всех доступных методов API для выключения
- # Проверяем наличие API перед использованием
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830082353.py b/.history/src/api/synology_20250830082353.py
deleted file mode 100644
index c78c04d..0000000
--- a/.history/src/api/synology_20250830082353.py
+++ /dev/null
@@ -1,1048 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830082444.py b/.history/src/api/synology_20250830082444.py
deleted file mode 100644
index beeca16..0000000
--- a/.history/src/api/synology_20250830082444.py
+++ /dev/null
@@ -1,1097 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830082500.py b/.history/src/api/synology_20250830082500.py
deleted file mode 100644
index beeca16..0000000
--- a/.history/src/api/synology_20250830082500.py
+++ /dev/null
@@ -1,1097 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Проверяем, действительна ли текущая сессия
- current_time = time.time()
- if self.sid and (current_time - self._last_auth_time) < self._auth_expiry:
- logger.debug("Using existing session, still valid")
- return True
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- try:
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug("Querying API info to determine optimal auth version")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 3)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Используем максимально поддерживаемую версию, но не выше 6
- auth_version = min(max_version, 6)
- else:
- logger.warning("Failed to query API info, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth version 3")
- auth_version = 3
- auth_path = "auth.cgi"
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Подробная информация для отладки
- logger.debug(f"Auth response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- return False
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = current_time
- logger.info("Successfully logged in to Synology NAS")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in to Synology NAS: Error code {error_code}")
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds")
- return False
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error: {str(e)}")
- logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}")
- return False
- except requests.RequestException as e:
- logger.error(f"Request error: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during login: {str(e)}", exc_info=True)
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830082853.py b/.history/src/api/synology_20250830082853.py
deleted file mode 100644
index 2964a3e..0000000
--- a/.history/src/api/synology_20250830082853.py
+++ /dev/null
@@ -1,1150 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- auth_versions_to_try = [6, 3, 2, 1] # Пробуем от более новых к более старым версиям
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "auth.cgi" # Значение по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Проверка валидности полученной сессии
- if not self._validate_session():
- logger.warning("Session validation failed, trying next auth version")
- self.sid = None
- continue
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "sid": self.sid
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Подробное логирование для отладки
- logger.debug(f"Response: {response.text[:500]}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
- return None
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info("Session may have expired, re-authenticating...")
- self.sid = None # Сбрасываем SID
- if self.login():
- logger.info("Re-authenticated, retrying API request...")
- # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания
- return self._make_api_request(api_name, method, version, params, False)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830082954.py b/.history/src/api/synology_20250830082954.py
deleted file mode 100644
index f1b3e29..0000000
--- a/.history/src/api/synology_20250830082954.py
+++ /dev/null
@@ -1,1189 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- auth_versions_to_try = [6, 3, 2, 1] # Пробуем от более новых к более старым версиям
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "auth.cgi" # Значение по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Проверка валидности полученной сессии
- if not self._validate_session():
- logger.warning("Session validation failed, trying next auth version")
- self.sid = None
- continue
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830083115.py b/.history/src/api/synology_20250830083115.py
deleted file mode 100644
index f1b3e29..0000000
--- a/.history/src/api/synology_20250830083115.py
+++ /dev/null
@@ -1,1189 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- auth_versions_to_try = [6, 3, 2, 1] # Пробуем от более новых к более старым версиям
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "auth.cgi" # Значение по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Проверка валидности полученной сессии
- if not self._validate_session():
- logger.warning("Session validation failed, trying next auth version")
- self.sid = None
- continue
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830084539.py b/.history/src/api/synology_20250830084539.py
deleted file mode 100644
index a226ac0..0000000
--- a/.history/src/api/synology_20250830084539.py
+++ /dev/null
@@ -1,1204 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- auth_versions_to_try = [6, 3, 2, 1] # Пробуем от более новых к более старым версиям
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "auth.cgi" # Значение по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "auth.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Проверка валидности полученной сессии
- if not self._validate_session():
- logger.warning("Session validation failed, trying next auth version")
- self.sid = None
- continue
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830084644.py b/.history/src/api/synology_20250830084644.py
deleted file mode 100644
index b31704e..0000000
--- a/.history/src/api/synology_20250830084644.py
+++ /dev/null
@@ -1,1218 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830084803.py b/.history/src/api/synology_20250830084803.py
deleted file mode 100644
index b31704e..0000000
--- a/.history/src/api/synology_20250830084803.py
+++ /dev/null
@@ -1,1218 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок (заглушка)"""
- logger.warning("Function get_shared_folders() is not implemented yet")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы (заглушка)"""
- logger.warning("Function get_system_load() is not implemented yet")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище (заглушка)"""
- logger.warning("Function get_storage_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Storage.CGI.Storage",
- "version": "1",
- "method": "load_info",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830090902.py b/.history/src/api/synology_20250830090902.py
deleted file mode 100644
index 8c4a557..0000000
--- a/.history/src/api/synology_20250830090902.py
+++ /dev/null
@@ -1,1317 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности (заглушка)"""
- logger.warning("Function get_security_status() is not implemented yet")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Здесь будет реализация после добавления соответствующего API
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": "SYNO.Core.SecurityScan.Status",
- "version": "1",
- "method": "get",
- "sid": self.sid
- }
-
- # В текущей версии просто возвращаем заглушку
- return {
- "success": False,
- "status": "not_implemented",
- "last_check": None,
- "is_secure": False,
- "error": "not_implemented"
- }
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830090936.py b/.history/src/api/synology_20250830090936.py
deleted file mode 100644
index fd6bba7..0000000
--- a/.history/src/api/synology_20250830090936.py
+++ /dev/null
@@ -1,1364 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830091024.py b/.history/src/api/synology_20250830091024.py
deleted file mode 100644
index 4b06eb0..0000000
--- a/.history/src/api/synology_20250830091024.py
+++ /dev/null
@@ -1,1480 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
diff --git a/.history/src/api/synology_20250830091124.py b/.history/src/api/synology_20250830091124.py
deleted file mode 100644
index 0517d6f..0000000
--- a/.history/src/api/synology_20250830091124.py
+++ /dev/null
@@ -1,1773 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830091218.py b/.history/src/api/synology_20250830091218.py
deleted file mode 100644
index 25d086c..0000000
--- a/.history/src/api/synology_20250830091218.py
+++ /dev/null
@@ -1,1903 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830092441.py b/.history/src/api/synology_20250830092441.py
deleted file mode 100644
index 25d086c..0000000
--- a/.history/src/api/synology_20250830092441.py
+++ /dev/null
@@ -1,1903 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton
- # Для других API обычно используется метод restart или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"reboot": "true"} # Передаем флаг для перезагрузки
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- methods_to_try = ["restart", "reboot"]
- result = None
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830095113.py b/.history/src/api/synology_20250830095113.py
deleted file mode 100644
index 018a5d3..0000000
--- a/.history/src/api/synology_20250830095113.py
+++ /dev/null
@@ -1,1907 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Пробуем использовать наиболее совместимые API для перезагрузки
- # SYNO.Core.System обычно доступен на большинстве систем
- logger.info("Trying reboot with SYNO.Core.System API")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=1)
-
- if result is None:
- # Если не сработало, пробуем альтернативные методы
- logger.info("Trying alternative reboot method with SYNO.DSM.System API")
- result = self._make_api_request("SYNO.DSM.System", "reboot", version=1)
-
- if result is None and SYNOLOGY_POWER_API != "SYNO.Core.System":
- # Пробуем настроенный в конфигурации API, если он отличается
- logger.info(f"Trying reboot with configured API: {SYNOLOGY_POWER_API}")
- methods_to_try = ["restart", "reboot"]
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for storage status request")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830095635.py b/.history/src/api/synology_20250830095635.py
deleted file mode 100644
index 672c37a..0000000
--- a/.history/src/api/synology_20250830095635.py
+++ /dev/null
@@ -1,1918 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Пробуем использовать наиболее совместимые API для перезагрузки
- # SYNO.Core.System обычно доступен на большинстве систем
- logger.info("Trying reboot with SYNO.Core.System API")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=1)
-
- if result is None:
- # Если не сработало, пробуем альтернативные методы
- logger.info("Trying alternative reboot method with SYNO.DSM.System API")
- result = self._make_api_request("SYNO.DSM.System", "reboot", version=1)
-
- if result is None and SYNOLOGY_POWER_API != "SYNO.Core.System":
- # Пробуем настроенный в конфигурации API, если он отличается
- logger.info(f"Trying reboot with configured API: {SYNOLOGY_POWER_API}")
- methods_to_try = ["restart", "reboot"]
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830095651.py b/.history/src/api/synology_20250830095651.py
deleted file mode 100644
index 672c37a..0000000
--- a/.history/src/api/synology_20250830095651.py
+++ /dev/null
@@ -1,1918 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Пробуем использовать наиболее совместимые API для перезагрузки
- # SYNO.Core.System обычно доступен на большинстве систем
- logger.info("Trying reboot with SYNO.Core.System API")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=1)
-
- if result is None:
- # Если не сработало, пробуем альтернативные методы
- logger.info("Trying alternative reboot method with SYNO.DSM.System API")
- result = self._make_api_request("SYNO.DSM.System", "reboot", version=1)
-
- if result is None and SYNOLOGY_POWER_API != "SYNO.Core.System":
- # Пробуем настроенный в конфигурации API, если он отличается
- logger.info(f"Trying reboot with configured API: {SYNOLOGY_POWER_API}")
- methods_to_try = ["restart", "reboot"]
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830100555.py b/.history/src/api/synology_20250830100555.py
deleted file mode 100644
index a7bf02b..0000000
--- a/.history/src/api/synology_20250830100555.py
+++ /dev/null
@@ -1,1944 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Пробуем использовать наиболее совместимые API для перезагрузки
- # SYNO.Core.System обычно доступен на большинстве систем
- logger.info("Trying reboot with SYNO.Core.System API")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=1)
-
- if result is None:
- # Если не сработало, пробуем альтернативные методы
- logger.info("Trying alternative reboot method with SYNO.DSM.System API")
- result = self._make_api_request("SYNO.DSM.System", "reboot", version=1)
-
- if result is None and SYNOLOGY_POWER_API != "SYNO.Core.System":
- # Пробуем настроенный в конфигурации API, если он отличается
- logger.info(f"Trying reboot with configured API: {SYNOLOGY_POWER_API}")
- methods_to_try = ["restart", "reboot"]
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Используем метод shutdown_system с исправленным API
- result = False
- try:
- # Пробуем использовать наиболее совместимый API для выключения
- logger.info("Trying shutdown with SYNO.Core.System API")
- api_result = self._make_api_request("SYNO.Core.System", "shutdown", version=1)
- if api_result is not None:
- result = True
- logger.info("Successfully initiated system shutdown using SYNO.Core.System API")
- except Exception as e:
- logger.error(f"Error during shutdown with SYNO.Core.System: {str(e)}")
-
- # Если не сработало, пробуем альтернативные методы
- if not result:
- try:
- logger.info("Trying alternative shutdown method with SYNO.DSM.System API")
- api_result = self._make_api_request("SYNO.DSM.System", "shutdown", version=1)
- if api_result is not None:
- result = True
- logger.info("Successfully initiated system shutdown using SYNO.DSM.System API")
- except Exception as e:
- logger.error(f"Error during shutdown with SYNO.DSM.System: {str(e)}")
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830101023.py b/.history/src/api/synology_20250830101023.py
deleted file mode 100644
index ace77ed..0000000
--- a/.history/src/api/synology_20250830101023.py
+++ /dev/null
@@ -1,1948 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Пробуем использовать наиболее совместимые API для перезагрузки
- # SYNO.Core.System обычно доступен на большинстве систем
- logger.info("Trying reboot with SYNO.Core.System API")
- result = self._make_api_request("SYNO.Core.System", "reboot", version=1)
-
- if result is None:
- # Если не сработало, пробуем альтернативные методы
- logger.info("Trying alternative reboot method with SYNO.DSM.System API")
- result = self._make_api_request("SYNO.DSM.System", "reboot", version=1)
-
- if result is None and SYNOLOGY_POWER_API != "SYNO.Core.System":
- # Пробуем настроенный в конфигурации API, если он отличается
- logger.info(f"Trying reboot with configured API: {SYNOLOGY_POWER_API}")
- methods_to_try = ["restart", "reboot"]
- for method in methods_to_try:
- result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION)
- if result is not None:
- logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}")
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Список API и методов для попытки выключения
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "shutdown", "version": 1},
- {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1},
- {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION}
- ]
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}")
- api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if api_result is not None:
- logger.info(f"Successfully initiated system shutdown using {api_info['api']} API")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All shutdown attempts failed")
- return False
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830101047.py b/.history/src/api/synology_20250830101047.py
deleted file mode 100644
index ce496b7..0000000
--- a/.history/src/api/synology_20250830101047.py
+++ /dev/null
@@ -1,1961 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Список API и методов для попытки перезагрузки
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "reboot", "version": 1},
- {"api": "SYNO.DSM.System", "method": "reboot", "version": 1},
- ]
-
- # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных
- if SYNOLOGY_POWER_API not in ["SYNO.Core.System", "SYNO.DSM.System"]:
- for method in ["restart", "reboot"]:
- apis_to_try.append({
- "api": SYNOLOGY_POWER_API,
- "method": method,
- "version": SYNOLOGY_API_VERSION
- })
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}")
- result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api_info['api']} API")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during reboot with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All reboot attempts failed")
- return False
- break
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Список API и методов для попытки выключения
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "shutdown", "version": 1},
- {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1},
- {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION}
- ]
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}")
- api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if api_result is not None:
- logger.info(f"Successfully initiated system shutdown using {api_info['api']} API")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All shutdown attempts failed")
- return False
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830101104.py b/.history/src/api/synology_20250830101104.py
deleted file mode 100644
index a174012..0000000
--- a/.history/src/api/synology_20250830101104.py
+++ /dev/null
@@ -1,1954 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Список API и методов для попытки перезагрузки
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "reboot", "version": 1},
- {"api": "SYNO.DSM.System", "method": "reboot", "version": 1},
- ]
-
- # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных
- if SYNOLOGY_POWER_API not in ["SYNO.Core.System", "SYNO.DSM.System"]:
- for method in ["restart", "reboot"]:
- apis_to_try.append({
- "api": SYNOLOGY_POWER_API,
- "method": method,
- "version": SYNOLOGY_API_VERSION
- })
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}")
- result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api_info['api']} API")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during reboot with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All reboot attempts failed")
- return False
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для перезагрузки
- apis_to_try = [
- {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"name": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"name": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Список API и методов для попытки выключения
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "shutdown", "version": 1},
- {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1},
- {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION}
- ]
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}")
- api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if api_result is not None:
- logger.info(f"Successfully initiated system shutdown using {api_info['api']} API")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All shutdown attempts failed")
- return False
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830101145.py b/.history/src/api/synology_20250830101145.py
deleted file mode 100644
index 53baace..0000000
--- a/.history/src/api/synology_20250830101145.py
+++ /dev/null
@@ -1,1944 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Список API и методов для попытки перезагрузки
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "reboot", "version": 1},
- {"api": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"api": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных
- already_added = [item["api"] for item in apis_to_try]
- if SYNOLOGY_POWER_API not in already_added:
- for method in ["restart", "reboot"]:
- apis_to_try.append({
- "api": SYNOLOGY_POWER_API,
- "method": method,
- "version": SYNOLOGY_API_VERSION
- })
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}")
- result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api_info['api']} API")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки
- return True
- else:
- # Успешный вызов API, но система не ушла оффлайн
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- except Exception as e:
- logger.error(f"Error during reboot with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All reboot attempts failed")
- return False
-
- # Получаем список доступных API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot"
- }
-
- logger.debug("Checking available reboot APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available reboot APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Список API и методов для попытки выключения
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "shutdown", "version": 1},
- {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1},
- {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION}
- ]
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}")
- api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if api_result is not None:
- logger.info(f"Successfully initiated system shutdown using {api_info['api']} API")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All shutdown attempts failed")
- return False
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830101211.py b/.history/src/api/synology_20250830101211.py
deleted file mode 100644
index 2a41bba..0000000
--- a/.history/src/api/synology_20250830101211.py
+++ /dev/null
@@ -1,1895 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Список API и методов для попытки перезагрузки
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "reboot", "version": 1},
- {"api": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"api": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных
- already_added = [item["api"] for item in apis_to_try]
- if SYNOLOGY_POWER_API not in already_added:
- for method in ["restart", "reboot"]:
- apis_to_try.append({
- "api": SYNOLOGY_POWER_API,
- "method": method,
- "version": SYNOLOGY_API_VERSION
- })
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}")
- result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api_info['api']} API")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки
- return True
- else:
- # Успешный вызов API, но система не ушла оффлайн
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- except Exception as e:
- logger.error(f"Error during reboot with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All reboot attempts failed")
- return False
- for api in apis_to_try:
- logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api['name']}")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Ждем, пока система снова станет доступна
- logger.info("Waiting for system to come back online...")
- return self.wait_for_boot(max_attempts=30, delay=10)
- else:
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- else:
- logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}")
-
- logger.error("Failed to reboot system after trying multiple APIs")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Список API и методов для попытки выключения
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "shutdown", "version": 1},
- {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1},
- {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION}
- ]
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}")
- api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if api_result is not None:
- logger.info(f"Successfully initiated system shutdown using {api_info['api']} API")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All shutdown attempts failed")
- return False
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830101236.py b/.history/src/api/synology_20250830101236.py
deleted file mode 100644
index 6d5a673..0000000
--- a/.history/src/api/synology_20250830101236.py
+++ /dev/null
@@ -1,1861 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Список API и методов для попытки перезагрузки
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "reboot", "version": 1},
- {"api": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"api": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных
- already_added = [item["api"] for item in apis_to_try]
- if SYNOLOGY_POWER_API not in already_added:
- for method in ["restart", "reboot"]:
- apis_to_try.append({
- "api": SYNOLOGY_POWER_API,
- "method": method,
- "version": SYNOLOGY_API_VERSION
- })
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}")
- result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api_info['api']} API")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки
- return True
- else:
- # Успешный вызов API, но система не ушла оффлайн
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- except Exception as e:
- logger.error(f"Error during reboot with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All reboot attempts failed")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Список API и методов для попытки выключения
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "shutdown", "version": 1},
- {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1},
- {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION}
- ]
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}")
- api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if api_result is not None:
- logger.info(f"Successfully initiated system shutdown using {api_info['api']} API")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All shutdown attempts failed")
- return False
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830101843.py b/.history/src/api/synology_20250830101843.py
deleted file mode 100644
index 6d5a673..0000000
--- a/.history/src/api/synology_20250830101843.py
+++ /dev/null
@@ -1,1861 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Список API и методов для попытки перезагрузки
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "reboot", "version": 1},
- {"api": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"api": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных
- already_added = [item["api"] for item in apis_to_try]
- if SYNOLOGY_POWER_API not in already_added:
- for method in ["restart", "reboot"]:
- apis_to_try.append({
- "api": SYNOLOGY_POWER_API,
- "method": method,
- "version": SYNOLOGY_API_VERSION
- })
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}")
- result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api_info['api']} API")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки
- return True
- else:
- # Успешный вызов API, но система не ушла оффлайн
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- except Exception as e:
- logger.error(f"Error during reboot with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All reboot attempts failed")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Список API и методов для попытки выключения
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "shutdown", "version": 1},
- {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1},
- {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION}
- ]
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}")
- api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if api_result is not None:
- logger.info(f"Successfully initiated system shutdown using {api_info['api']} API")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All shutdown attempts failed")
- return False
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Получаем расписание через API
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not result:
- return {}
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {}
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830104812.py b/.history/src/api/synology_20250830104812.py
deleted file mode 100644
index cf9b72d..0000000
--- a/.history/src/api/synology_20250830104812.py
+++ /dev/null
@@ -1,1873 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Список API и методов для попытки перезагрузки
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "reboot", "version": 1},
- {"api": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"api": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных
- already_added = [item["api"] for item in apis_to_try]
- if SYNOLOGY_POWER_API not in already_added:
- for method in ["restart", "reboot"]:
- apis_to_try.append({
- "api": SYNOLOGY_POWER_API,
- "method": method,
- "version": SYNOLOGY_API_VERSION
- })
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}")
- result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api_info['api']} API")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки
- return True
- else:
- # Успешный вызов API, но система не ушла оффлайн
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- except Exception as e:
- logger.error(f"Error during reboot with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All reboot attempts failed")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Список API и методов для попытки выключения
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "shutdown", "version": 1},
- {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1},
- {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION}
- ]
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}")
- api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if api_result is not None:
- logger.info(f"Successfully initiated system shutdown using {api_info['api']} API")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All shutdown attempts failed")
- return False
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Пробуем сначала более новый API
- result = self._make_api_request("SYNO.Core.System.PowerSchedule", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- result = self._make_api_request("SYNO.Core.System", "get_power_schedule", version=1)
-
- if not result:
- # Если нет результатов, вернем структуру, которую ожидает код
- logger.warning("PowerSchedule API not available, returning empty schedule structure")
- return {
- "boot_tasks": [],
- "shutdown_tasks": []
- }
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {
- "boot_tasks": [],
- "shutdown_tasks": []
- }
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Получаем текущее расписание
- current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1)
-
- if not current_schedule:
- logger.error("Failed to get current power schedule")
- return False
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params)
-
- if not result:
- logger.error("Failed to set power schedule")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830104833.py b/.history/src/api/synology_20250830104833.py
deleted file mode 100644
index 30b422b..0000000
--- a/.history/src/api/synology_20250830104833.py
+++ /dev/null
@@ -1,1877 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Список API и методов для попытки перезагрузки
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "reboot", "version": 1},
- {"api": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"api": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных
- already_added = [item["api"] for item in apis_to_try]
- if SYNOLOGY_POWER_API not in already_added:
- for method in ["restart", "reboot"]:
- apis_to_try.append({
- "api": SYNOLOGY_POWER_API,
- "method": method,
- "version": SYNOLOGY_API_VERSION
- })
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}")
- result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api_info['api']} API")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки
- return True
- else:
- # Успешный вызов API, но система не ушла оффлайн
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- except Exception as e:
- logger.error(f"Error during reboot with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All reboot attempts failed")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Список API и методов для попытки выключения
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "shutdown", "version": 1},
- {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1},
- {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION}
- ]
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}")
- api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if api_result is not None:
- logger.info(f"Successfully initiated system shutdown using {api_info['api']} API")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All shutdown attempts failed")
- return False
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Пробуем сначала более новый API
- result = self._make_api_request("SYNO.Core.System.PowerSchedule", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- result = self._make_api_request("SYNO.Core.System", "get_power_schedule", version=1)
-
- if not result:
- # Если нет результатов, вернем структуру, которую ожидает код
- logger.warning("PowerSchedule API not available, returning empty schedule structure")
- return {
- "boot_tasks": [],
- "shutdown_tasks": []
- }
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {
- "boot_tasks": [],
- "shutdown_tasks": []
- }
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Пробуем сначала более новый API
- api_name = "SYNO.Core.System.PowerSchedule"
- method = "set"
- version = 1
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request(api_name, method, version, params=params)
-
- if not result:
- # Пробуем альтернативный API
- api_name = "SYNO.Core.System"
- method = "set_power_schedule"
- result = self._make_api_request(api_name, method, version, params=params)
-
- if not result:
- logger.error("Failed to set power schedule with any available API")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully with {api_name}")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830104945.py b/.history/src/api/synology_20250830104945.py
deleted file mode 100644
index 30b422b..0000000
--- a/.history/src/api/synology_20250830104945.py
+++ /dev/null
@@ -1,1877 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Список API и методов для попытки перезагрузки
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "reboot", "version": 1},
- {"api": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"api": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных
- already_added = [item["api"] for item in apis_to_try]
- if SYNOLOGY_POWER_API not in already_added:
- for method in ["restart", "reboot"]:
- apis_to_try.append({
- "api": SYNOLOGY_POWER_API,
- "method": method,
- "version": SYNOLOGY_API_VERSION
- })
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}")
- result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api_info['api']} API")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки
- return True
- else:
- # Успешный вызов API, но система не ушла оффлайн
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- except Exception as e:
- logger.error(f"Error during reboot with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All reboot attempts failed")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Список API и методов для попытки выключения
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "shutdown", "version": 1},
- {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1},
- {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION}
- ]
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}")
- api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if api_result is not None:
- logger.info(f"Successfully initiated system shutdown using {api_info['api']} API")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All shutdown attempts failed")
- return False
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Пробуем сначала более новый API
- result = self._make_api_request("SYNO.Core.System.PowerSchedule", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- result = self._make_api_request("SYNO.Core.System", "get_power_schedule", version=1)
-
- if not result:
- # Если нет результатов, вернем структуру, которую ожидает код
- logger.warning("PowerSchedule API not available, returning empty schedule structure")
- return {
- "boot_tasks": [],
- "shutdown_tasks": []
- }
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {
- "boot_tasks": [],
- "shutdown_tasks": []
- }
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Пробуем сначала более новый API
- api_name = "SYNO.Core.System.PowerSchedule"
- method = "set"
- version = 1
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request(api_name, method, version, params=params)
-
- if not result:
- # Пробуем альтернативный API
- api_name = "SYNO.Core.System"
- method = "set_power_schedule"
- result = self._make_api_request(api_name, method, version, params=params)
-
- if not result:
- logger.error("Failed to set power schedule with any available API")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully with {api_name}")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830105105.py b/.history/src/api/synology_20250830105105.py
deleted file mode 100644
index ca6117c..0000000
--- a/.history/src/api/synology_20250830105105.py
+++ /dev/null
@@ -1,1894 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Список API и методов для попытки перезагрузки
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "reboot", "version": 1},
- {"api": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"api": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных
- already_added = [item["api"] for item in apis_to_try]
- if SYNOLOGY_POWER_API not in already_added:
- for method in ["restart", "reboot"]:
- apis_to_try.append({
- "api": SYNOLOGY_POWER_API,
- "method": method,
- "version": SYNOLOGY_API_VERSION
- })
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}")
- result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api_info['api']} API")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки
- return True
- else:
- # Успешный вызов API, но система не ушла оффлайн
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- except Exception as e:
- logger.error(f"Error during reboot with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All reboot attempts failed")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Список API и методов для попытки выключения
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "shutdown", "version": 1},
- {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1},
- {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION}
- ]
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}")
- api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if api_result is not None:
- logger.info(f"Successfully initiated system shutdown using {api_info['api']} API")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All shutdown attempts failed")
- return False
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Список возможных API для получения расписания питания
- apis_to_try = [
- {"api": "SYNO.Core.System.PowerSchedule", "method": "get", "version": 1},
- {"api": "SYNO.Core.System", "method": "get_power_schedule", "version": 1},
- {"api": "SYNO.Core.Power", "method": "schedule", "version": 1},
- {"api": "SYNO.Core.Power.Schedule", "method": "get", "version": 1},
- {"api": "SYNO.PowerScheduler", "method": "load", "version": 1},
- {"api": "SYNO.PowerSchedule", "method": "get", "version": 1}
- ]
-
- result = {}
- # Пробуем все возможные API по очереди
- for api_config in apis_to_try:
- logger.debug(f"Trying API: {api_config['api']}.{api_config['method']} v{api_config['version']}")
- temp_result = self._make_api_request(
- api_config["api"],
- api_config["method"],
- version=api_config["version"]
- )
- if temp_result:
- logger.info(f"Successfully retrieved power schedule using {api_config['api']}.{api_config['method']}")
- result = temp_result
- break
-
- if not result:
- # Если нет результатов, вернем структуру, которую ожидает код
- logger.warning("No PowerSchedule API available, returning empty schedule structure")
- return {
- "boot_tasks": [],
- "shutdown_tasks": []
- }
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {
- "boot_tasks": [],
- "shutdown_tasks": []
- }
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Пробуем сначала более новый API
- api_name = "SYNO.Core.System.PowerSchedule"
- method = "set"
- version = 1
-
- # Подготавливаем новое расписание
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Устанавливаем новое расписание
- result = self._make_api_request(api_name, method, version, params=params)
-
- if not result:
- # Пробуем альтернативный API
- api_name = "SYNO.Core.System"
- method = "set_power_schedule"
- result = self._make_api_request(api_name, method, version, params=params)
-
- if not result:
- logger.error("Failed to set power schedule with any available API")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully with {api_name}")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830105130.py b/.history/src/api/synology_20250830105130.py
deleted file mode 100644
index 145921f..0000000
--- a/.history/src/api/synology_20250830105130.py
+++ /dev/null
@@ -1,1908 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Список API и методов для попытки перезагрузки
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "reboot", "version": 1},
- {"api": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"api": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных
- already_added = [item["api"] for item in apis_to_try]
- if SYNOLOGY_POWER_API not in already_added:
- for method in ["restart", "reboot"]:
- apis_to_try.append({
- "api": SYNOLOGY_POWER_API,
- "method": method,
- "version": SYNOLOGY_API_VERSION
- })
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}")
- result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api_info['api']} API")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки
- return True
- else:
- # Успешный вызов API, но система не ушла оффлайн
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- except Exception as e:
- logger.error(f"Error during reboot with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All reboot attempts failed")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Список API и методов для попытки выключения
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "shutdown", "version": 1},
- {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1},
- {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION}
- ]
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}")
- api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if api_result is not None:
- logger.info(f"Successfully initiated system shutdown using {api_info['api']} API")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All shutdown attempts failed")
- return False
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Список возможных API для получения расписания питания
- apis_to_try = [
- {"api": "SYNO.Core.System.PowerSchedule", "method": "get", "version": 1},
- {"api": "SYNO.Core.System", "method": "get_power_schedule", "version": 1},
- {"api": "SYNO.Core.Power", "method": "schedule", "version": 1},
- {"api": "SYNO.Core.Power.Schedule", "method": "get", "version": 1},
- {"api": "SYNO.PowerScheduler", "method": "load", "version": 1},
- {"api": "SYNO.PowerSchedule", "method": "get", "version": 1}
- ]
-
- result = {}
- # Пробуем все возможные API по очереди
- for api_config in apis_to_try:
- logger.debug(f"Trying API: {api_config['api']}.{api_config['method']} v{api_config['version']}")
- temp_result = self._make_api_request(
- api_config["api"],
- api_config["method"],
- version=api_config["version"]
- )
- if temp_result:
- logger.info(f"Successfully retrieved power schedule using {api_config['api']}.{api_config['method']}")
- result = temp_result
- break
-
- if not result:
- # Если нет результатов, вернем структуру, которую ожидает код
- logger.warning("No PowerSchedule API available, returning empty schedule structure")
- return {
- "boot_tasks": [],
- "shutdown_tasks": []
- }
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {
- "boot_tasks": [],
- "shutdown_tasks": []
- }
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Подготавливаем базовые параметры расписания
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Список возможных API для установки расписания питания
- apis_to_try = [
- {"api": "SYNO.Core.System.PowerSchedule", "method": "set", "version": 1},
- {"api": "SYNO.Core.System", "method": "set_power_schedule", "version": 1},
- {"api": "SYNO.Core.Power", "method": "set_schedule", "version": 1},
- {"api": "SYNO.Core.Power.Schedule", "method": "set", "version": 1},
- {"api": "SYNO.PowerScheduler", "method": "save", "version": 1},
- {"api": "SYNO.PowerSchedule", "method": "set", "version": 1}
- ]
-
- success = False
- last_used_api = ""
-
- # Пробуем все возможные API по очереди
- for api_config in apis_to_try:
- api_name = api_config["api"]
- method = api_config["method"]
- version = api_config["version"]
-
- logger.debug(f"Trying to set power schedule with API: {api_name}.{method} v{version}")
- result = self._make_api_request(api_name, method, version, params=params)
-
- if result:
- logger.info(f"Successfully set power schedule using {api_name}.{method}")
- success = True
- last_used_api = api_name
- break
-
- if not success:
- logger.error("Failed to set power schedule with any available API")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully with {last_used_api}")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/api/synology_20250830110338.py b/.history/src/api/synology_20250830110338.py
deleted file mode 100644
index 145921f..0000000
--- a/.history/src/api/synology_20250830110338.py
+++ /dev/null
@@ -1,1908 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для взаимодействия с API Synology NAS
-"""
-
-import requests
-from requests.adapters import HTTPAdapter
-import json
-import logging
-import time
-import urllib3
-from urllib3.util import Retry
-from typing import Dict, Any, Optional, List
-import socket
-import struct
-from time import sleep
-
-from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_USERNAME,
- SYNOLOGY_PASSWORD,
- SYNOLOGY_SECURE,
- SYNOLOGY_TIMEOUT,
- SYNOLOGY_MAC,
- WOL_PORT,
- SYNOLOGY_API_VERSION,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API
-)
-from src.api.api_discovery import discover_available_apis, find_compatible_api
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-logger = logging.getLogger(__name__)
-
-class SynologyAPI:
- """Класс для взаимодействия с API Synology NAS"""
-
- def __init__(self):
- """Инициализация класса SynologyAPI"""
- logger.info("Creating API with auto-retry and connection pool")
- logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}")
-
- self.protocol = "https" if SYNOLOGY_SECURE else "http"
- self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
- self.sid = None
- self.session = requests.Session()
-
- # Настройка SSL
- if self.protocol == "https":
- logger.debug("SSL enabled, disabling certificate verification for internal network")
- self.session.verify = False # Отключаем проверку SSL для внутренней сети
-
- # Добавляем пользовательские заголовки для улучшения совместимости с API
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
- self.session.headers.update(custom_headers)
- logger.debug("Added browser-like headers for API compatibility")
-
- # Добавляем повторные попытки для HTTP-запросов
- retry_strategy = Retry(
- total=5, # Увеличиваем количество попыток
- status_forcelist=[429, 500, 502, 503, 504, 404],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.5, # Увеличиваем задержку между попытками
- respect_retry_after_header=True
- )
- adapter = HTTPAdapter(
- max_retries=retry_strategy,
- pool_connections=3,
- pool_maxsize=10
- )
- self.session.mount("http://", adapter)
- self.session.mount("https://", adapter)
-
- # Таймауты будут указаны в запросах
- self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2)
- logger.debug(f"Setting default request timeout: {self.default_timeout}")
-
- # Кэш для хранения результатов запросов
- self._cache = {}
- self._cache_ttl = {}
- self._last_online_check = 0
- self._last_online_status = False
- self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса
-
- # Время последней успешной аутентификации и срок действия сессии
- self._last_auth_time = 0
- self._auth_expiry = 3600 # По умолчанию 1 час
-
- # Информация о доступных API
- self._available_apis = {}
- self._api_info_ttl = 0
-
- # Инициализируем API version resolver для автоматического определения совместимых API
- self.api_resolver = None # Будет создан при необходимости
-
- def login(self) -> bool:
- """Авторизация в API Synology NAS"""
- # Сбрасываем SID для новой сессии
- self.sid = None
-
- logger.info("Attempting to authenticate with Synology NAS...")
- logger.debug(f"Base URL: {self.base_url}")
-
- # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки
- # Избегаем вызова is_online(), чтобы не создавать рекурсию
- online_status = self._check_tcp_connection()
- if not online_status:
- logger.error("Cannot login: Synology NAS is not reachable")
- return False
-
- # Пробуем различные версии API для аутентификации
- # Начинаем с версии 3, которая показала лучшую совместимость в тестах
- auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии
-
- for auth_version in auth_versions_to_try:
- try:
- # Определяем путь к API аутентификации
- auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию
-
- # Проверка информации API для определения доступных версий API
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- logger.debug(f"Querying API info for auth version {auth_version}")
- try:
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {})
- max_version = auth_info.get("maxVersion", 6)
- min_version = auth_info.get("minVersion", 1)
- auth_path = auth_info.get("path", "entry.cgi")
- logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}")
-
- # Проверяем поддержку текущей версии
- if auth_version < min_version or auth_version > max_version:
- logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version")
- continue
- else:
- logger.warning("Failed to query API info, using default auth path")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default auth path")
-
- # Основной запрос авторизации
- url = f"{self.base_url}/{auth_path}"
- params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "SynologyPowerControlBot",
- "format": "cookie"
- }
-
- # Для версии 6+ используем немного другой формат
- if auth_version >= 6:
- params["enable_syno_token"] = "yes"
-
- logger.debug(f"Sending auth request to {url} with API version {auth_version}")
- start_time = time.time()
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code}")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error("Failed to decode JSON response")
- logger.debug(f"Response content: {response.text[:200]}")
- continue # Пробуем следующую версию
-
- if data.get("success"):
- self.sid = data.get("data", {}).get("sid")
- self._last_auth_time = time.time()
- logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}")
- logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...")
-
- # Получаем и сохраняем токен SYNO, если он есть
- syno_token = data.get("data", {}).get("synotoken")
- if syno_token:
- self.session.headers.update({'X-SYNO-TOKEN': syno_token})
- logger.debug("Added X-SYNO-TOKEN header for improved API compatibility")
-
- # Также добавляем SID в cookies для улучшения совместимости
- self.session.cookies.update({
- 'id': self.sid,
- 'sid': self.sid
- })
- logger.debug("Added SID to session cookies for improved compatibility")
-
- # Проверка валидности полученной сессии с помощью простого запроса
- # Будем использовать SYNO.API.Info без проверки сложных методов
-
- # Даем системе немного времени для инициализации сессии
- time.sleep(0.5)
-
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}")
-
- # Если ошибка связана с версией API, пробуем следующую версию
- if error_code in [104, 105]:
- logger.warning(f"Auth version {auth_version} not supported, trying next version")
- continue
-
- # Дополнительная диагностика
- if error_code == 400:
- logger.error("Authentication error: Invalid credentials")
- elif error_code == 401:
- logger.error("Authentication error: Account disabled")
- elif error_code == 402:
- logger.error("Authentication error: Permission denied")
- elif error_code == 403:
- logger.error("Authentication error: 2-factor authentication required")
- elif error_code == 404:
- logger.error("Authentication error: Failed to authenticate with 2-factor authentication")
-
- # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API
- if error_code in [400, 401, 402, 403, 404]:
- return False
-
- except requests.exceptions.Timeout:
- logger.error(f"Connection timeout during auth with version {auth_version}")
- continue # Пробуем следующую версию
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except requests.RequestException as e:
- logger.error(f"Request error during auth with version {auth_version}: {str(e)}")
- continue # Пробуем следующую версию
- except Exception as e:
- logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True)
- continue # Пробуем следующую версию
-
- # Если все версии не сработали
- logger.error("Failed to authenticate with any API version")
- return False
-
- def _validate_session(self) -> bool:
- """Проверяет валидность сессии после авторизации"""
- if not self.sid:
- return False
-
- # Попробуем сделать простой запрос для проверки сессии
- test_apis = [
- {"api": "SYNO.Core.System", "method": "info", "version": 1},
- {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1}
- ]
-
- for test_api in test_apis:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": test_api["api"],
- "version": str(test_api["version"]),
- "method": test_api["method"],
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False)
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.debug(f"Session validation successful using {test_api['api']}")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- if error_code != 119: # Не сессия истекла
- logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}")
- return True # Считаем сессию валидной, если ошибка не связана с истечением сессии
- except Exception as e:
- logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}")
-
- logger.warning("Session validation failed with all test APIs")
- return False
-
- def logout(self) -> bool:
- """Выход из API Synology NAS"""
- if not self.sid:
- return True
-
- try:
- url = f"{self.base_url}/auth.cgi"
- params = {
- "api": "SYNO.API.Auth",
- "version": "1",
- "method": "logout",
- "session": "SynologyPowerControlBot",
- "_sid": self.sid
- }
-
- response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False)
- data = response.json()
-
- if data.get("success"):
- self.sid = None
- logger.info("Successfully logged out from Synology NAS")
- return True
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.error(f"Failed to log out from Synology NAS: Error code {error_code}")
- return False
-
- except requests.RequestException as e:
- logger.error(f"Connection error: {str(e)}")
- return False
-
- def _make_api_request(self, api_name: str, method: str, version: int = 1,
- params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]:
- """Обобщенный метод для выполнения API запросов с обработкой ошибок"""
- # Ограничение на количество повторных попыток
- if retry_count >= 3:
- logger.error(f"Too many retries for {api_name}.{method}, giving up")
- return None
-
- # Проверка наличия авторизации
- if not self.sid and not self.login():
- logger.error(f"Not authenticated for API request: {api_name}.{method}")
- return None
-
- # Проверка информации API для определения пути и поддерживаемой версии
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": api_name
- }
-
- api_path = "entry.cgi"
- try:
- logger.debug(f"Querying API info for {api_name}")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- api_info = api_info_data.get("data", {}).get(api_name, {})
- if api_info:
- max_version = api_info.get("maxVersion", version)
- min_version = api_info.get("minVersion", version)
- api_path = api_info.get("path", "entry.cgi")
-
- # Проверка, поддерживается ли запрошенная версия
- if version < min_version:
- logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}")
- version = min_version
- elif version > max_version:
- logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}")
- version = max_version
-
- logger.debug(f"Using API path: {api_path}, version: {version}")
- else:
- logger.warning(f"API {api_name} not found in API info, using defaults")
- except Exception as e:
- logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults")
-
- # Подготовка базовых параметров запроса
- base_params = {
- "api": api_name,
- "version": str(version),
- "method": method,
- "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости
- }
-
- # Добавление дополнительных параметров, если они заданы
- if params:
- base_params.update(params)
-
- url = f"{self.base_url}/{api_path}"
- logger.debug(f"API request: {api_name}.{method} v{version} to {url}")
- logger.debug(f"Full request params: {base_params}")
-
- try:
- start_time = time.time()
- response = self.session.get(
- url,
- params=base_params,
- timeout=self.default_timeout,
- verify=False
- )
- elapsed_time = time.time() - start_time
- logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}")
-
- # Проверка статуса HTTP
- if response.status_code != 200:
- logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}")
-
- # Повторная попытка при ошибках соединения
- if response.status_code in [500, 502, 503, 504]:
- logger.info(f"Server error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- try:
- data = response.json()
- except json.JSONDecodeError:
- logger.error(f"Failed to decode JSON response for {api_name}.{method}")
- logger.debug(f"Response content: {response.text[:200]}")
-
- # Повторная попытка при ошибках декодирования
- logger.info(f"JSON decode error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- if data.get("success"):
- logger.info(f"API request successful for {api_name}.{method}")
- return data.get("data", {})
- else:
- error_code = data.get("error", {}).get("code", -1)
- error_desc = self._get_error_description(error_code)
- logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}")
-
- # Ошибки доступа или прав часто встречаются, но они не критичные
- # Например, ошибка 102 означает, что нет прав, но NAS доступен
- if error_code in [102, 103, 104, 105]:
- logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}")
- # Возвращаем пустой словарь вместо None,
- # чтобы вызывающий код мог понять, что запрос выполнен
- return {}
-
- # Если ошибка связана с авторизацией и нам разрешено повторить попытку
- if error_code in [106, 107, 119] and retry_auth:
- logger.info(f"Session error (code {error_code}), creating fresh session...")
- self.sid = None # Сбрасываем SID
-
- # Для ошибки 119 (Session timeout) дадим системе немного времени
- if error_code == 119:
- logger.info("Session timeout detected, waiting before retry...")
- sleep(3)
-
- if self.login():
- logger.info("Re-authenticated with fresh session, retrying API request...")
- # Рекурсивный вызов, но со счетчиком повторов
- return self._make_api_request(api_name, method, version, params, False, retry_count + 1)
-
- # Для некоторых ошибок можно автоматически повторить запрос
- if error_code in [408, 429, 500, 502, 503, 504]:
- logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
-
- except requests.exceptions.Timeout:
- logger.error(f"Request timeout for {api_name}.{method}")
-
- # Повторная попытка при таймауте
- if retry_count < 2:
- logger.info(f"Timeout, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except requests.exceptions.ConnectionError as e:
- logger.error(f"Connection error for {api_name}.{method}: {str(e)}")
-
- # Повторная попытка при ошибке соединения
- if retry_count < 2:
- logger.info(f"Connection error, retrying request for {api_name}.{method}")
- sleep(2)
- return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1)
-
- return None
- except Exception as e:
- logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}")
- return None
-
- def get_system_status(self) -> Dict[str, Any]:
- """Получение статуса системы"""
- # Проверяем доступность системы
- if not self.is_online():
- logger.info("Device is offline, skipping API request")
- return {"status": "offline"}
-
- # Проверяем, есть ли кэшированный результат
- cache_key = "system_status"
- current_time = time.time()
- if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60:
- logger.debug("Using cached system status")
- return self._cache[cache_key]
-
- # Используем рекомендованный API для получения информации о системе
- logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info
- if SYNOLOGY_INFO_API == "SYNO.DSM.Info":
- method = "getinfo"
- else:
- method = "get"
-
- result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION)
-
- if result:
- logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": SYNOLOGY_INFO_API
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если основной API не сработал, пробуем резервные варианты
- logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs")
-
- # Пробуем резервные API
- apis_to_try = [
- {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2},
- {"name": "SYNO.Core.System", "method": "info", "version": 1},
- {"name": "SYNO.Core.System.Status", "method": "get", "version": 1},
- {"name": "SYNO.Core.System.Info", "method": "get", "version": 1},
- ]
-
- for api in apis_to_try:
- if api["name"] == SYNOLOGY_INFO_API:
- continue # Пропускаем уже проверенный API
-
- logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result:
- logger.info(f"Successfully retrieved system info using {api['name']}")
-
- # Формируем расширенный ответ с дополнительной информацией
- system_info = {
- "status": "online",
- "hostname": result.get("hostname", "unknown"),
- "model": result.get("model", "unknown"),
- "version": result.get("version", "unknown"),
- "uptime": result.get("uptime", 0),
- "time": current_time,
- "is_online": True,
- "api_used": api["name"]
- }
-
- # Сохраняем в кэше
- self._cache[cache_key] = system_info
- self._cache_ttl[cache_key] = current_time
-
- return system_info
-
- # Если все запросы не удались, но система онлайн, возвращаем базовую информацию
- logger.warning("Failed to retrieve system info with all API methods")
- return {
- "status": "error",
- "error": "Failed to fetch system information",
- "is_online": True,
- "time": current_time
- }
-
- def shutdown_system(self) -> bool:
- """Выключение системы"""
- # Проверяем, включено ли устройство перед попыткой его выключить
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline, no need to shut down")
- return True
-
- logger.info("Attempting to shutdown Synology NAS...")
-
- # Попробуем сначала использовать предпочтительный API для управления питанием
- logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}")
-
- # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState
- # Для других API обычно используется метод shutdown или reboot
- if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery":
- # Для этого API нужны специальные параметры
- params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания
- result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params)
- else:
- # Пробуем стандартный метод
- result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION)
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods")
-
- # Если не сработал основной метод, пробуем резервные варианты
- # Проверка всех доступных методов API для выключения
- apis_to_try = [
- {"name": "SYNO.Core.System", "method": "shutdown", "version": 3},
- {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.System.Power", "method": "shutdown", "version": 1},
- {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1}
- ]
-
- # Проверяем доступные API
- try:
- api_info_url = f"{self.base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System"
- }
-
- logger.debug("Checking available shutdown APIs")
- api_info_response = self.session.get(
- api_info_url,
- params=api_info_params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if api_info_response.status_code == 200:
- api_info_data = api_info_response.json()
- if api_info_data.get("success"):
- available_apis = api_info_data.get("data", {})
- logger.debug(f"Available APIs: {list(available_apis.keys())}")
-
- # Фильтруем только доступные API
- filtered_apis = []
- for api in apis_to_try:
- if api["name"] in available_apis:
- api_info = available_apis[api["name"]]
- # Проверка версии API
- if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1):
- filtered_apis.append(api)
- logger.debug(f"Adding {api['name']} to available shutdown APIs")
- else:
- logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}")
-
- if filtered_apis:
- apis_to_try = filtered_apis
- else:
- logger.warning("No compatible APIs found, trying all methods as fallback")
- else:
- logger.warning("Failed to query API info, using default methods")
- else:
- logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods")
- except Exception as e:
- logger.warning(f"Error querying API info: {str(e)}, using default methods")
-
- # Пробуем все доступные методы по порядку
- for api in apis_to_try:
- logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}")
- result = self._make_api_request(api["name"], api["method"], version=api["version"])
-
- if result is not None:
- logger.info(f"Successfully initiated system shutdown using {api['name']}")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- # Проверяем статус
- if not self.is_online(force_check=True):
- logger.info("System is now offline. Shutdown confirmed successful.")
- return True
- else:
- logger.info("System still appears to be online, but shutdown may be in progress.")
- return True
- else:
- logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}")
-
- # Если ни один метод не сработал, но система стала недоступна
- if not self.is_online(force_check=True):
- logger.info("System appears to be shutting down despite API errors")
- return True
-
- logger.error("Failed to shutdown system after trying multiple APIs")
- return False
-
- def reboot_system(self) -> bool:
- """Перезагрузка системы"""
- # Проверяем, включена ли система
- if not self.is_online(force_check=True):
- logger.error("Cannot reboot: System is offline")
- return False
-
- logger.info("Attempting to reboot Synology NAS...")
-
- # Список API и методов для попытки перезагрузки
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "reboot", "version": 1},
- {"api": "SYNO.DSM.System", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1},
- {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.System", "method": "reboot", "version": 3},
- {"api": "SYNO.System.Power", "method": "reboot", "version": 1},
- {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1}
- ]
-
- # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных
- already_added = [item["api"] for item in apis_to_try]
- if SYNOLOGY_POWER_API not in already_added:
- for method in ["restart", "reboot"]:
- apis_to_try.append({
- "api": SYNOLOGY_POWER_API,
- "method": method,
- "version": SYNOLOGY_API_VERSION
- })
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}")
- result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if result is not None:
- logger.info(f"Successfully initiated system reboot using {api_info['api']} API")
-
- # Даем системе время начать процесс перезагрузки
- logger.info("Waiting for reboot to initialize...")
- sleep(5)
-
- # Ждем, пока система станет недоступна (признак перезагрузки)
- reboot_started = False
- for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек)
- if not self.is_online(force_check=True):
- logger.info(f"System went offline after {i*5} seconds, reboot in progress")
- reboot_started = True
- break
- logger.debug(f"System still online, waiting... ({i+1}/12)")
- sleep(5)
-
- if reboot_started:
- # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки
- return True
- else:
- # Успешный вызов API, но система не ушла оффлайн
- logger.warning("System did not go offline after reboot command, but command was accepted")
- # Даже если система не ушла оффлайн, команда могла быть принята
- return True
- except Exception as e:
- logger.error(f"Error during reboot with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All reboot attempts failed")
- return False
-
- def _get_error_description(self, error_code: int) -> str:
- """Получение описания ошибки по коду"""
- error_descriptions = {
- 100: "Unknown error",
- 101: "Invalid parameter",
- 102: "API does not exist",
- 103: "Method does not exist",
- 104: "Version does not support",
- 105: "Permission denied",
- 106: "Session timeout",
- 107: "Session interrupted by duplicate login",
- 400: "Invalid credentials",
- 401: "Account disabled",
- 402: "Permission denied",
- 403: "2FA required",
- 404: "Failed to authenticate with 2FA"
- }
- return error_descriptions.get(error_code, "Unknown error code")
-
- def _check_tcp_connection(self) -> bool:
- """Проверка базового TCP-соединения с Synology NAS"""
- try:
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- return result == 0
- except socket.error as e:
- logger.error(f"Socket error during connection check: {str(e)}")
- return False
- except Exception as e:
- logger.error(f"Unexpected error during connection check: {str(e)}")
- return False
-
- def is_online(self, force_check=False) -> bool:
- """Проверка онлайн-статуса Synology NAS"""
- # Используем кэшированное значение, если доступно и не устарело
- current_time = time.time()
- if not force_check and (current_time - self._last_online_check) < self._online_check_interval:
- logger.debug(f"Using cached online status: {self._last_online_status}")
- return self._last_online_status
-
- logger.info("Checking if NAS is online...")
-
- # Проверяем TCP-соединение
- online_status = self._check_tcp_connection()
- logger.info(f"Detected Synology NAS online status: {online_status}")
-
- # Если TCP-соединение успешно и у нас есть действующий SID,
- # попробуем более детальную проверку через API
- if online_status and self.sid:
- logger.info("Trying to fetch more detailed online status through API...")
-
- # Пробуем разные API для проверки онлайн-статуса
- api_checks = [
- {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"},
- {"api": "SYNO.Core.System", "version": "1", "method": "info"},
- {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"}
- ]
-
- api_success = False
- for api_check in api_checks:
- try:
- url = f"{self.base_url}/entry.cgi"
- params = {
- "api": api_check["api"],
- "version": api_check["version"],
- "method": api_check["method"],
- "sid": self.sid
- }
-
- logger.debug(f"Trying online status check with {api_check['api']}")
- response = self.session.get(
- url,
- params=params,
- timeout=self.default_timeout,
- verify=False
- )
-
- if response.status_code == 200:
- data = response.json()
- if data.get("success"):
- logger.info(f"API request successful for {api_check['api']}")
- logger.info("Synology NAS is online with API access")
- api_success = True
- break
- else:
- error_code = data.get("error", {}).get("code", -1)
- logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable")
- else:
- logger.warning(f"API returned status code {response.status_code}")
- except Exception as e:
- logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}")
-
- if not api_success:
- logger.warning("All API checks failed, but TCP connection is successful")
-
- # Обновляем кэшированное значение
- self._last_online_check = current_time
- self._last_online_status = online_status
-
- return online_status
-
- def wake_on_lan(self) -> bool:
- """Отправка Wake-on-LAN пакета для включения Synology NAS"""
- if not SYNOLOGY_MAC:
- logger.error("MAC address not configured")
- return False
-
- try:
- # Преобразование MAC-адреса в байты
- mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '')
- if len(mac_address) != 12:
- logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}")
- return False
-
- try:
- mac_bytes = bytes.fromhex(mac_address)
- except ValueError as e:
- logger.error(f"Failed to parse MAC address: {str(e)}")
- logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}")
- return False
-
- # Создание Magic Packet
- magic_packet = b'\xff' * 6 + mac_bytes * 16
-
- # Отправка пакета на конкретный адрес
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}")
- except Exception as e:
- logger.error(f"Error sending directed WoL packet: {str(e)}")
- return False
-
- # Для надежности отправляем также широковещательный пакет
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
-
- # Используем стандартный широковещательный адрес
- broadcast_addr = "255.255.255.255"
- sock.sendto(magic_packet, (broadcast_addr, WOL_PORT))
- sock.close()
- logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}")
- except Exception as e:
- logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}")
- # Не считаем ошибкой, т.к. основной пакет уже отправлен
-
- return True
-
- except Exception as e:
- logger.error(f"Unexpected error in wake_on_lan: {str(e)}")
- return False
-
- def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool:
- """Ожидание загрузки Synology NAS"""
- logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...")
-
- for attempt in range(max_attempts):
- # Принудительно проверяем статус без использования кэша
- if self.is_online(force_check=True):
- logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)")
-
- # Проверяем, что не только сеть доступна, но и API загрузился
- api_ready = False
- logger.info("Waiting for API services to initialize...")
-
- for api_check in range(5): # Даем еще до 50 секунд для загрузки API
- if self.sid or self.login():
- api_ready = True
- logger.info(f"API services are ready after {api_check + 1} attempts")
- break
- logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)")
- sleep(10)
-
- if not api_ready:
- logger.warning("System is online but API services may not be fully initialized")
-
- # Дадим дополнительное время для полной загрузки всех сервисов
- sleep(delay)
- return True
-
- sleep(delay)
- logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}")
-
- logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds")
- return False
-
- def power_on(self) -> bool:
- """Включение Synology NAS"""
- # Принудительная проверка статуса
- if self.is_online(force_check=True):
- logger.info("Synology NAS is already online")
- return True
-
- logger.info("Powering on Synology NAS via Wake-on-LAN...")
-
- # Проверяем, настроен ли MAC-адрес
- if not SYNOLOGY_MAC:
- logger.error("Cannot power on: MAC address not configured in settings")
- return False
-
- # Пробуем отправить несколько WoL пакетов для надежности
- success = False
- for attempt in range(3):
- logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3")
- if self.wake_on_lan():
- success = True
- break
- sleep(1)
-
- if not success:
- logger.error("Failed to send Wake-on-LAN packets")
- return False
-
- # Ожидание загрузки
- logger.info("WoL packets sent successfully, waiting for system to boot...")
- boot_result = self.wait_for_boot(max_attempts=30, delay=10)
-
- if boot_result:
- # Проверяем доступность API после загрузки
- system_status = self.get_system_status()
- if system_status.get("status") == "online":
- logger.info("System booted successfully with API access")
- return True
- else:
- logger.warning("System appears to be online but API may not be fully ready")
- return True
- else:
- logger.error("System did not come online after WoL")
- return False
-
- def power_off(self) -> bool:
- """Выключение Synology NAS"""
- if not self.is_online(force_check=True):
- logger.info("Synology NAS is already offline")
- return True
-
- logger.info("Powering off Synology NAS...")
-
- # Список API и методов для попытки выключения
- apis_to_try = [
- {"api": "SYNO.Core.System", "method": "shutdown", "version": 1},
- {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1},
- {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION}
- ]
-
- # Перебираем все возможные API и методы
- for api_info in apis_to_try:
- try:
- logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}")
- api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version'])
- if api_result is not None:
- logger.info(f"Successfully initiated system shutdown using {api_info['api']} API")
-
- # Даем системе время начать процесс выключения
- logger.info("Waiting for shutdown to initialize...")
- sleep(5)
-
- return True
- except Exception as e:
- logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}")
-
- # Если все попытки не удались, возвращаем False
- logger.error("All shutdown attempts failed")
- return False
-
- # Если все еще не сработало, используем оригинальный метод shutdown_system
- if not result:
- result = self.shutdown_system()
-
- if result:
- # Дополнительная проверка, что система действительно выключилась
- logger.info("Verifying system is offline...")
- for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек)
- if not self.is_online(force_check=True):
- logger.info(f"System confirmed offline after {attempt * 10} seconds")
- return True
- logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)")
- sleep(10)
-
- logger.warning("System still appears to be online after shutdown command")
- return False
- else:
- logger.error("Failed to initiate shutdown")
- return False
-
- # Заглушки для расширенных методов
- def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- logger.info("Getting list of shared folders")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for shared folders request")
- return []
-
- try:
- # Запрашиваем список общих папок через FileStation API
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for shared folders")
- alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1)
- if alt_result:
- return alt_result.get("shares", [])
- return []
-
- return result.get("shares", [])
-
- except Exception as e:
- logger.error(f"Error getting shared folders: {str(e)}")
- return []
-
- def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о загрузке системы"""
- logger.info("Getting system load information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system load request")
- return {}
-
- try:
- # Запрашиваем информацию о загрузке системы
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system load")
- alt_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not alt_result:
- return {}
-
- # Формируем из частичных данных
- return {
- "cpu_load": alt_result.get("cpu_usage", 0),
- "memory": {
- "total": alt_result.get("memory_size", 0),
- "used": alt_result.get("memory_usage", 0),
- "usage_percent": alt_result.get("memory_usage_percent", 0)
- }
- }
-
- # Формируем структурированный результат
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-
- except Exception as e:
- logger.error(f"Error getting system load: {str(e)}")
- return {}
-
- def is_online_api(self) -> bool:
- """Проверка онлайн-статуса Synology NAS с использованием API"""
- if not self.is_online():
- return False
-
- # Проверяем доступность API через авторизацию
- if not self.sid and not self.login():
- return False
-
- return True
-
- def get_storage_status(self) -> Dict[str, Any]:
- """Получение подробной информации о хранилище"""
- logger.info("Getting storage status information")
-
- # Проверяем доступность NAS и API
- if not self.is_online_api():
- logger.error("Cannot get storage status: NAS is not online or API is not accessible")
- return {"error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API
- result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for storage info")
- alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- if not alt_result:
- # Пробуем еще один альтернативный API
- logger.info("Trying SYNO.Core.System API for storage info")
- sys_result = self._make_api_request("SYNO.Core.System", "info", version=3)
-
- if not sys_result:
- return {
- "volumes": [],
- "disks": [],
- "total_size": 0,
- "total_used": 0,
- "error": "no_data"
- }
-
- # Извлекаем базовую информацию о хранилище из системной информации
- return {
- "volumes": [],
- "disks": [],
- "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты
- "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2,
- }
-
- # Обрабатываем данные из альтернативного API
- volumes = alt_result.get("volumes", [])
- disks = alt_result.get("disks", [])
-
- else:
- # Обрабатываем данные из основного API
- volumes = result.get("volumes", [])
- disks = result.get("disks", [])
-
- # Рассчитываем общие размеры
- total_size = 0
- total_used = 0
-
- for volume in volumes:
- volume_size = volume.get("size", {}).get("total", 0)
- volume_used = volume.get("size", {}).get("used", 0)
-
- total_size += volume_size
- total_used += volume_used
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-
- except Exception as e:
- logger.error(f"Error in get_storage_status: {str(e)}")
- return {"error": str(e)}
-
- def get_security_status(self) -> Dict[str, Any]:
- """Получение информации о состоянии безопасности"""
- logger.info("Getting security status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for security status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о безопасности через API Security Scan
- result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1)
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for security status")
- # Проверяем статус брандмауэра
- firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1)
-
- # Проверяем статус автоматических обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Если ни один из API не отвечает
- if not firewall_result and not update_result:
- # Получаем общую информацию о системе для базовой проверки безопасности
- sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not sys_result:
- return {
- "success": False,
- "status": "unknown",
- "last_check": None,
- "is_secure": False,
- "error": "no_security_api"
- }
-
- # Собираем базовые сведения из системной информации
- return {
- "success": True,
- "status": "basic",
- "last_check": None,
- "is_secure": True, # Предполагаем, что система в целом безопасна
- "firewall_enabled": None,
- "auto_update": None,
- "version_latest": sys_result.get("version_string", "")
- }
-
- # Собираем информацию из доступных результатов
- firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None
- auto_update = update_result.get("auto_update", False) if update_result else None
-
- # Определяем, насколько система безопасна
- is_secure = True # По умолчанию предполагаем, что система безопасна
- if firewall_enabled is not None and not firewall_enabled:
- is_secure = False
-
- return {
- "success": True,
- "status": "partial",
- "last_check": None,
- "is_secure": is_secure,
- "firewall_enabled": firewall_enabled,
- "auto_update": auto_update
- }
-
- # Если основное API отвечает, возвращаем его данные
- return {
- "success": True,
- "status": result.get("status", "unknown"),
- "last_check": result.get("last_check", None),
- "is_secure": result.get("is_secure", False),
- "details": result.get("details", {})
- }
-
- except Exception as e:
- logger.error(f"Error in get_security_status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение списка активных процессов"""
- logger.info(f"Getting list of active processes (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for processes request")
- return []
-
- try:
- # Получаем список процессов через API
- result = self._make_api_request("SYNO.Core.System.Process", "list", version=1,
- params={"sort_by": "cpu", "order": "DESC", "limit": limit})
-
- if not result:
- logger.warning("Failed to get process list")
- return []
-
- return result.get("processes", [])
-
- except Exception as e:
- logger.error(f"Error getting process list: {str(e)}")
- return []
-
- def get_network_status(self) -> Dict[str, Any]:
- """Получение информации о сетевых подключениях"""
- logger.info("Getting network status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for network status request")
- return {}
-
- try:
- # Получаем информацию о сетевых интерфейсах
- interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1)
-
- # Получаем статистику использования сети
- utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- interfaces = []
- if interface_result:
- interfaces = interface_result.get("interfaces", [])
-
- network_stats = {}
- if utilization_result and "network" in utilization_result:
- network_stats = utilization_result.get("network", {})
-
- # Объединяем данные
- for interface in interfaces:
- iface_id = interface.get("id", "")
- if iface_id in network_stats:
- interface["rx"] = network_stats[iface_id].get("rx", 0)
- interface["tx"] = network_stats[iface_id].get("tx", 0)
-
- return {
- "interfaces": interfaces,
- "statistics": network_stats
- }
-
- except Exception as e:
- logger.error(f"Error getting network status: {str(e)}")
- return {}
-
- def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]:
- """Получение журналов системы"""
- logger.info(f"Getting system logs (limit={limit})")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for system logs request")
- return []
-
- try:
- # Получаем журналы через API
- result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if not result:
- # Пробуем альтернативный API
- logger.info("Trying alternative API for system logs")
- alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1,
- params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"})
-
- if alt_result:
- return alt_result.get("logs", [])
- return []
-
- return result.get("logs", [])
-
- except Exception as e:
- logger.error(f"Error getting system logs: {str(e)}")
- return []
-
- def get_power_schedule(self) -> Dict[str, Any]:
- """Получение расписания включения/выключения"""
- logger.info("Getting power schedule")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for power schedule request")
- return {}
-
- try:
- # Список возможных API для получения расписания питания
- apis_to_try = [
- {"api": "SYNO.Core.System.PowerSchedule", "method": "get", "version": 1},
- {"api": "SYNO.Core.System", "method": "get_power_schedule", "version": 1},
- {"api": "SYNO.Core.Power", "method": "schedule", "version": 1},
- {"api": "SYNO.Core.Power.Schedule", "method": "get", "version": 1},
- {"api": "SYNO.PowerScheduler", "method": "load", "version": 1},
- {"api": "SYNO.PowerSchedule", "method": "get", "version": 1}
- ]
-
- result = {}
- # Пробуем все возможные API по очереди
- for api_config in apis_to_try:
- logger.debug(f"Trying API: {api_config['api']}.{api_config['method']} v{api_config['version']}")
- temp_result = self._make_api_request(
- api_config["api"],
- api_config["method"],
- version=api_config["version"]
- )
- if temp_result:
- logger.info(f"Successfully retrieved power schedule using {api_config['api']}.{api_config['method']}")
- result = temp_result
- break
-
- if not result:
- # Если нет результатов, вернем структуру, которую ожидает код
- logger.warning("No PowerSchedule API available, returning empty schedule structure")
- return {
- "boot_tasks": [],
- "shutdown_tasks": []
- }
-
- return result
-
- except Exception as e:
- logger.error(f"Error getting power schedule: {str(e)}")
- return {
- "boot_tasks": [],
- "shutdown_tasks": []
- }
-
- def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool:
- """Настройка расписания включения/выключения
-
- Args:
- schedule_type: Тип расписания ('boot' или 'shutdown')
- days: Список дней недели (0-6, где 0 - понедельник)
- time: Время в формате 'HH:MM'
- enabled: Включить или выключить расписание
-
- Returns:
- True если успешно, False в противном случае
- """
- logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for setting power schedule")
- return False
-
- try:
- # Подготавливаем базовые параметры расписания
- params = {
- "enabled": enabled,
- "type": schedule_type,
- "day": days,
- "time": time
- }
-
- # Список возможных API для установки расписания питания
- apis_to_try = [
- {"api": "SYNO.Core.System.PowerSchedule", "method": "set", "version": 1},
- {"api": "SYNO.Core.System", "method": "set_power_schedule", "version": 1},
- {"api": "SYNO.Core.Power", "method": "set_schedule", "version": 1},
- {"api": "SYNO.Core.Power.Schedule", "method": "set", "version": 1},
- {"api": "SYNO.PowerScheduler", "method": "save", "version": 1},
- {"api": "SYNO.PowerSchedule", "method": "set", "version": 1}
- ]
-
- success = False
- last_used_api = ""
-
- # Пробуем все возможные API по очереди
- for api_config in apis_to_try:
- api_name = api_config["api"]
- method = api_config["method"]
- version = api_config["version"]
-
- logger.debug(f"Trying to set power schedule with API: {api_name}.{method} v{version}")
- result = self._make_api_request(api_name, method, version, params=params)
-
- if result:
- logger.info(f"Successfully set power schedule using {api_name}.{method}")
- success = True
- last_used_api = api_name
- break
-
- if not success:
- logger.error("Failed to set power schedule with any available API")
- return False
-
- logger.info(f"Power schedule for {schedule_type} set successfully with {last_used_api}")
- return True
-
- except Exception as e:
- logger.error(f"Error setting power schedule: {str(e)}")
- return False
-
- def get_temperature_status(self) -> Dict[str, Any]:
- """Получение информации о температуре системы и дисков"""
- logger.info("Getting temperature status")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for temperature status request")
- return {}
-
- try:
- # Получаем информацию о системе для общей температуры
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- # Получаем информацию о дисках для их температуры
- storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- system_temp = None
- disk_temps = []
-
- if system_info:
- system_temp = system_info.get("temperature")
-
- if storage_info:
- disks = storage_info.get("disks", [])
- for disk in disks:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temp", None)
- if temp is not None:
- disk_temps.append({
- "name": name,
- "model": model,
- "temperature": temp
- })
-
- return {
- "system_temperature": system_temp,
- "disk_temperatures": disk_temps,
- "warning": system_info.get("temperature_warn", False) if system_info else False
- }
-
- except Exception as e:
- logger.error(f"Error getting temperature status: {str(e)}")
- return {}
-
- def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Просмотр файлов в указанной директории
-
- Args:
- folder_path: Путь к папке (пустая строка для корневых общих папок)
- limit: Максимальное количество элементов для возврата
-
- Returns:
- Словарь с информацией о файлах и папках
- """
- logger.info(f"Browsing files in {folder_path or 'root'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file browsing")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Если путь не указан, получаем список общих папок
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- logger.error("Failed to list shared folders")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("shares", []),
- "path": "",
- "is_root": True
- }
- else:
- # Получаем список файлов в указанной директории
- params = {
- "folder_path": folder_path,
- "limit": limit,
- "offset": 0,
- "sort_by": "name",
- "sort_direction": "ASC"
- }
-
- result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to list files in {folder_path}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "items": result.get("files", []),
- "path": folder_path,
- "is_root": False,
- "total": result.get("total", 0)
- }
-
- except Exception as e:
- logger.error(f"Error browsing files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]:
- """Управление системным сервисом
-
- Args:
- service_name: Имя сервиса
- action: Действие (status/start/stop/restart)
-
- Returns:
- Словарь с результатом операции
- """
- logger.info(f"Managing service {service_name}, action: {action}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for service management")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Проверяем доступное API для управления сервисами
- if action == "status":
- result = self._make_api_request("SYNO.Core.Service", "get", version=1,
- params={"service": service_name})
- else:
- result = self._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
-
- if not result:
- logger.error(f"Failed to {action} service {service_name}")
- return {"success": False, "error": "api_error"}
-
- return {
- "success": True,
- "service": service_name,
- "action": action,
- "result": result,
- "status": result.get("status") if action == "status" else "completed"
- }
-
- except Exception as e:
- logger.error(f"Error managing service {service_name}: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]:
- """Поиск файлов по шаблону
-
- Args:
- pattern: Шаблон для поиска
- folder_path: Путь к папке для поиска (пустая строка для всех общих папок)
- limit: Максимальное количество результатов
-
- Returns:
- Словарь с найденными файлами
- """
- logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for file search")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- if not folder_path:
- # Получаем список всех общих папок для поиска
- shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not shares_result:
- logger.error("Failed to list shared folders for search")
- return {"success": False, "error": "api_error"}
-
- # Формируем список путей для поиска
- folder_paths = [share.get("path") for share in shares_result.get("shares", [])]
- else:
- folder_paths = [folder_path]
-
- # Запускаем поиск
- params = {
- "folder_path": folder_paths,
- "pattern": pattern,
- "limit": limit,
- "offset": 0
- }
-
- result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params)
-
- if not result:
- logger.error(f"Failed to start search for {pattern}")
- return {"success": False, "error": "api_error"}
-
- # Получаем taskid для проверки результатов
- taskid = result.get("taskid")
- if not taskid:
- logger.error("No taskid received for search")
- return {"success": False, "error": "no_task_id"}
-
- # Ожидаем завершения поиска
- search_result = {"finished": False, "progress": 0}
- for _ in range(10): # Максимум 10 попыток
- search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2,
- params={"taskid": taskid})
-
- if not search_status:
- break
-
- search_result["progress"] = search_status.get("progress", 0)
-
- if search_status.get("finished", False):
- search_result["finished"] = True
- break
-
- time.sleep(0.5) # Пауза между запросами
-
- # Получаем результаты поиска
- if search_result["finished"]:
- list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2,
- params={"taskid": taskid, "limit": limit})
-
- if list_result:
- files = list_result.get("files", [])
- return {
- "success": True,
- "pattern": pattern,
- "results": files,
- "total": list_result.get("total", len(files))
- }
-
- # Если не удалось получить результаты, останавливаем поиск
- self._make_api_request("SYNO.FileStation.Search", "stop", version=2,
- params={"taskid": taskid})
-
- return {
- "success": False,
- "error": "search_timeout",
- "progress": search_result["progress"]
- }
-
- except Exception as e:
- logger.error(f"Error searching files: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_backup_status(self) -> Dict[str, Any]:
- """Получение информации о резервном копировании"""
- logger.info("Getting backup status information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for backup status request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Пробуем получить информацию о Hyper Backup
- hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1)
-
- # Пробуем получить информацию о задачах Time Backup
- time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1)
-
- # Проверяем статус резервного копирования USB
- usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1)
-
- backups = {
- "hyper_backup": hyper_result.get("backups", []) if hyper_result else [],
- "time_backup": time_result.get("tasks", []) if time_result else [],
- "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False}
- }
-
- return {
- "success": True,
- "backups": backups,
- "available_apis": {
- "hyper_backup": hyper_result is not None,
- "time_backup": time_result is not None,
- "usb_copy": usb_result is not None
- }
- }
-
- except Exception as e:
- logger.error(f"Error getting backup status: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def check_for_updates(self) -> Dict[str, Any]:
- """Проверка наличия обновлений системы"""
- logger.info("Checking for system updates")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for update check")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем текущую информацию о системе
- system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2)
-
- if not system_info:
- logger.error("Failed to get system info for update check")
- return {"success": False, "error": "api_error"}
-
- # Проверяем наличие обновлений
- update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1)
-
- # Получаем настройки автоматического обновления
- settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1)
-
- # Получаем информацию о доступных обновлениях
- update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1)
-
- current_version = system_info.get("version_string", "unknown")
- auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False
-
- updates = []
- if update_info and "updates" in update_info:
- updates = update_info.get("updates", [])
-
- update_available = len(updates) > 0
-
- return {
- "success": True,
- "current_version": current_version,
- "update_available": update_available,
- "auto_update_enabled": auto_update_enabled,
- "updates": updates
- }
-
- except Exception as e:
- logger.error(f"Error checking for updates: {str(e)}")
- return {"success": False, "error": str(e)}
-
- def get_quota_info(self) -> Dict[str, Any]:
- """Получение информации о квотах пользователей"""
- logger.info("Getting user quota information")
-
- # Аутентифицируемся перед запросом данных
- if not self.sid and not self.login():
- logger.error("Failed to authenticate for quota info request")
- return {"success": False, "error": "authentication_failed"}
-
- try:
- # Получаем список пользователей
- users_result = self._make_api_request("SYNO.Core.User", "list", version=1)
-
- if not users_result:
- logger.error("Failed to get user list for quota info")
- return {"success": False, "error": "api_error"}
-
- users = users_result.get("users", [])
- user_quotas = []
-
- # Получаем квоты для каждого пользователя
- for user in users:
- user_name = user.get("name")
- if not user_name:
- continue
-
- quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1,
- params={"user_name": user_name})
-
- if quota_result and "quotas" in quota_result:
- user_quotas.append({
- "user": user_name,
- "quotas": quota_result.get("quotas", [])
- })
-
- return {
- "success": True,
- "user_quotas": user_quotas
- }
-
- except Exception as e:
- logger.error(f"Error getting quota info: {str(e)}")
- return {"success": False, "error": str(e)}
diff --git a/.history/src/bot_20250830063649.py b/.history/src/bot_20250830063649.py
deleted file mode 100644
index 51929ad..0000000
--- a/.history/src/bot_20250830063649.py
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.utils.logger import setup_logging
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=Application.ALL_UPDATES)
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830063839.py b/.history/src/bot_20250830063839.py
deleted file mode 100644
index 51929ad..0000000
--- a/.history/src/bot_20250830063839.py
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.utils.logger import setup_logging
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=Application.ALL_UPDATES)
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830065301.py b/.history/src/bot_20250830065301.py
deleted file mode 100644
index 1a3e7a8..0000000
--- a/.history/src/bot_20250830065301.py
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command
-)
-from src.utils.logger import setup_logging
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=Application.ALL_UPDATES)
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830065311.py b/.history/src/bot_20250830065311.py
deleted file mode 100644
index 2967c3c..0000000
--- a/.history/src/bot_20250830065311.py
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command
-)
-from src.utils.logger import setup_logging
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=Application.ALL_UPDATES)
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830065454.py b/.history/src/bot_20250830065454.py
deleted file mode 100644
index 2967c3c..0000000
--- a/.history/src/bot_20250830065454.py
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command
-)
-from src.utils.logger import setup_logging
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=Application.ALL_UPDATES)
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830072835.py b/.history/src/bot_20250830072835.py
deleted file mode 100644
index b336351..0000000
--- a/.history/src/bot_20250830072835.py
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command
-)
-from src.utils.logger import setup_logging
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling()
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830072844.py b/.history/src/bot_20250830072844.py
deleted file mode 100644
index b336351..0000000
--- a/.history/src/bot_20250830072844.py
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command
-)
-from src.utils.logger import setup_logging
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling()
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830075657.py b/.history/src/bot_20250830075657.py
deleted file mode 100644
index cbf8422..0000000
--- a/.history/src/bot_20250830075657.py
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command
-)
-from src.utils.logger import setup_logging
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling()
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830075723.py b/.history/src/bot_20250830075723.py
deleted file mode 100644
index f272445..0000000
--- a/.history/src/bot_20250830075723.py
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application: Application = None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830075740.py b/.history/src/bot_20250830075740.py
deleted file mode 100644
index 7620586..0000000
--- a/.history/src/bot_20250830075740.py
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830075757.py b/.history/src/bot_20250830075757.py
deleted file mode 100644
index 7620586..0000000
--- a/.history/src/bot_20250830075757.py
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830083325.py b/.history/src/bot_20250830083325.py
deleted file mode 100644
index d875d34..0000000
--- a/.history/src/bot_20250830083325.py
+++ /dev/null
@@ -1,103 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830083341.py b/.history/src/bot_20250830083341.py
deleted file mode 100644
index 031bb35..0000000
--- a/.history/src/bot_20250830083341.py
+++ /dev/null
@@ -1,104 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830083502.py b/.history/src/bot_20250830083502.py
deleted file mode 100644
index 031bb35..0000000
--- a/.history/src/bot_20250830083502.py
+++ /dev/null
@@ -1,104 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830091533.py b/.history/src/bot_20250830091533.py
deleted file mode 100644
index 733f399..0000000
--- a/.history/src/bot_20250830091533.py
+++ /dev/null
@@ -1,119 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830091644.py b/.history/src/bot_20250830091644.py
deleted file mode 100644
index bfabb6b..0000000
--- a/.history/src/bot_20250830091644.py
+++ /dev/null
@@ -1,134 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.command_handlers import (
- start_command,
- help_command,
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830092152.py b/.history/src/bot_20250830092152.py
deleted file mode 100644
index e6dadab..0000000
--- a/.history/src/bot_20250830092152.py
+++ /dev/null
@@ -1,136 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830092440.py b/.history/src/bot_20250830092440.py
deleted file mode 100644
index e6dadab..0000000
--- a/.history/src/bot_20250830092440.py
+++ /dev/null
@@ -1,136 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830093455.py b/.history/src/bot_20250830093455.py
deleted file mode 100644
index 93f7edd..0000000
--- a/.history/src/bot_20250830093455.py
+++ /dev/null
@@ -1,139 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- power_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830093513.py b/.history/src/bot_20250830093513.py
deleted file mode 100644
index 066a504..0000000
--- a/.history/src/bot_20250830093513.py
+++ /dev/null
@@ -1,141 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- power_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback))
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830093531.py b/.history/src/bot_20250830093531.py
deleted file mode 100644
index e272c26..0000000
--- a/.history/src/bot_20250830093531.py
+++ /dev/null
@@ -1,141 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- power_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчика callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$"))
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830093606.py b/.history/src/bot_20250830093606.py
deleted file mode 100644
index f9f0a2c..0000000
--- a/.history/src/bot_20250830093606.py
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- power_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^reboot_|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830093645.py b/.history/src/bot_20250830093645.py
deleted file mode 100644
index fc47ec1..0000000
--- a/.history/src/bot_20250830093645.py
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- advanced_power_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^reboot_|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830093703.py b/.history/src/bot_20250830093703.py
deleted file mode 100644
index b378a07..0000000
--- a/.history/src/bot_20250830093703.py
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- advanced_power_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^reboot_|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830094738.py b/.history/src/bot_20250830094738.py
deleted file mode 100644
index b378a07..0000000
--- a/.history/src/bot_20250830094738.py
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- advanced_power_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^reboot_|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830100755.py b/.history/src/bot_20250830100755.py
deleted file mode 100644
index c4f475c..0000000
--- a/.history/src/bot_20250830100755.py
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- advanced_power_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков callback-запросов
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830100926.py b/.history/src/bot_20250830100926.py
deleted file mode 100644
index 54d7e0e..0000000
--- a/.history/src/bot_20250830100926.py
+++ /dev/null
@@ -1,144 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- advanced_power_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков callback-запросов
- # Сначала обрабатываем более специфичные паттерны
- application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- # Затем более общие паттерны
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830101843.py b/.history/src/bot_20250830101843.py
deleted file mode 100644
index 54d7e0e..0000000
--- a/.history/src/bot_20250830101843.py
+++ /dev/null
@@ -1,144 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- advanced_power_callback
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков callback-запросов
- # Сначала обрабатываем более специфичные паттерны
- application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- # Затем более общие паттерны
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830110611.py b/.history/src/bot_20250830110611.py
deleted file mode 100644
index 468e0ae..0000000
--- a/.history/src/bot_20250830110611.py
+++ /dev/null
@@ -1,149 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- advanced_power_callback
-)
-from src.utils.admin_utils import (
- add_admin,
- remove_admin,
- list_admins
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков callback-запросов
- # Сначала обрабатываем более специфичные паттерны
- application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- # Затем более общие паттерны
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830110630.py b/.history/src/bot_20250830110630.py
deleted file mode 100644
index 58a915d..0000000
--- a/.history/src/bot_20250830110630.py
+++ /dev/null
@@ -1,154 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- advanced_power_callback
-)
-from src.utils.admin_utils import (
- add_admin,
- remove_admin,
- list_admins
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков для управления администраторами
- application.add_handler(CommandHandler("addadmin", add_admin))
- application.add_handler(CommandHandler("removeadmin", remove_admin))
- application.add_handler(CommandHandler("admins", list_admins))
-
- # Регистрация обработчиков callback-запросов
- # Сначала обрабатываем более специфичные паттерны
- application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- # Затем более общие паттерны
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830110906.py b/.history/src/bot_20250830110906.py
deleted file mode 100644
index 58a915d..0000000
--- a/.history/src/bot_20250830110906.py
+++ /dev/null
@@ -1,154 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- advanced_power_callback
-)
-from src.utils.admin_utils import (
- add_admin,
- remove_admin,
- list_admins
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков для управления администраторами
- application.add_handler(CommandHandler("addadmin", add_admin))
- application.add_handler(CommandHandler("removeadmin", remove_admin))
- application.add_handler(CommandHandler("admins", list_admins))
-
- # Регистрация обработчиков callback-запросов
- # Сначала обрабатываем более специфичные паттерны
- application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- # Затем более общие паттерны
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830141501.py b/.history/src/bot_20250830141501.py
deleted file mode 100644
index 97acdcc..0000000
--- a/.history/src/bot_20250830141501.py
+++ /dev/null
@@ -1,155 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- ConversationHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- advanced_power_callback
-)
-from src.utils.admin_utils import (
- add_admin,
- remove_admin,
- list_admins
-)
-from src.utils.logger import setup_logging
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков для управления администраторами
- application.add_handler(CommandHandler("addadmin", add_admin))
- application.add_handler(CommandHandler("removeadmin", remove_admin))
- application.add_handler(CommandHandler("admins", list_admins))
-
- # Регистрация обработчиков callback-запросов
- # Сначала обрабатываем более специфичные паттерны
- application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- # Затем более общие паттерны
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830141515.py b/.history/src/bot_20250830141515.py
deleted file mode 100644
index 919fd40..0000000
--- a/.history/src/bot_20250830141515.py
+++ /dev/null
@@ -1,158 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- ConversationHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- advanced_power_callback
-)
-from src.utils.admin_utils import (
- add_admin,
- remove_admin,
- list_admins
-)
-from src.utils.logger import setup_logging
-from src.agents.file_manager_agent import create_file_manager_handler
-from src.api.synology import SynologyAPI
-from src.api.filestation import add_file_manager_methods_to_synology_api
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков для управления администраторами
- application.add_handler(CommandHandler("addadmin", add_admin))
- application.add_handler(CommandHandler("removeadmin", remove_admin))
- application.add_handler(CommandHandler("admins", list_admins))
-
- # Регистрация обработчиков callback-запросов
- # Сначала обрабатываем более специфичные паттерны
- application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- # Затем более общие паттерны
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830141529.py b/.history/src/bot_20250830141529.py
deleted file mode 100644
index 4db220b..0000000
--- a/.history/src/bot_20250830141529.py
+++ /dev/null
@@ -1,165 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- ConversationHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- advanced_power_callback
-)
-from src.utils.admin_utils import (
- add_admin,
- remove_admin,
- list_admins
-)
-from src.utils.logger import setup_logging
-from src.agents.file_manager_agent import create_file_manager_handler
-from src.api.synology import SynologyAPI
-from src.api.filestation import add_file_manager_methods_to_synology_api
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков для управления администраторами
- application.add_handler(CommandHandler("addadmin", add_admin))
- application.add_handler(CommandHandler("removeadmin", remove_admin))
- application.add_handler(CommandHandler("admins", list_admins))
-
- # Регистрация обработчиков callback-запросов
- # Сначала обрабатываем более специфичные паттерны
- application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- # Затем более общие паттерны
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Создание экземпляра API и добавление методов для работы с файловой системой
- synology_api = SynologyAPI()
-
- # Регистрация обработчика файлового менеджера
- file_manager_handler = create_file_manager_handler(synology_api)
- application.add_handler(file_manager_handler)
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/bot_20250830141957.py b/.history/src/bot_20250830141957.py
deleted file mode 100644
index 4db220b..0000000
--- a/.history/src/bot_20250830141957.py
+++ /dev/null
@@ -1,165 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Главный модуль запуска телеграм-бота для управления Synology NAS
-"""
-
-import os
-import signal
-import sys
-import asyncio
-import logging
-from telegram.ext import (
- Application,
- CommandHandler,
- CallbackQueryHandler,
- MessageHandler,
- ConversationHandler,
- filters
-)
-
-from src.config.config import TELEGRAM_TOKEN
-from src.handlers.help_handlers import (
- start_command,
- help_command
-)
-from src.handlers.command_handlers import (
- status_command,
- power_command,
- power_callback
-)
-from src.handlers.extended_handlers import (
- storage_command,
- shares_command,
- system_command,
- load_command,
- security_command,
- check_api_command
-)
-from src.handlers.advanced_handlers import (
- processes_command,
- network_command,
- temperature_command,
- schedule_command,
- browse_command,
- search_command,
- updates_command,
- backup_command,
- quickreboot_command,
- reboot_command,
- sleep_command,
- wakeup_command,
- quota_command,
- schedule_callback,
- browse_callback,
- advanced_power_callback
-)
-from src.utils.admin_utils import (
- add_admin,
- remove_admin,
- list_admins
-)
-from src.utils.logger import setup_logging
-from src.agents.file_manager_agent import create_file_manager_handler
-from src.api.synology import SynologyAPI
-from src.api.filestation import add_file_manager_methods_to_synology_api
-
-async def shutdown(application: Application) -> None:
- """Корректное завершение работы бота"""
- logger = logging.getLogger(__name__)
- logger.info("Stopping Synology Power Control Bot...")
-
- # Останавливаем прием обновлений
- await application.stop()
- logger.info("Bot stopped successfully")
-
-def signal_handler(sig, frame, application=None):
- """Обработчик сигналов для корректного завершения"""
- logger = logging.getLogger(__name__)
- logger.info(f"Received signal {sig}, shutting down gracefully")
-
- if application:
- # Создаем и запускаем задачу завершения в event loop
- loop = asyncio.get_event_loop()
- if loop.is_running():
- loop.create_task(shutdown(application))
- else:
- loop.run_until_complete(shutdown(application))
-
- sys.exit(0)
-
-def main() -> None:
- """Основная функция запуска бота"""
- # Настройка логирования
- setup_logging()
- logger = logging.getLogger(__name__)
-
- # Проверка наличия токена
- if not TELEGRAM_TOKEN:
- logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.")
- return
-
- # Создание и настройка приложения бота
- logger.info("Starting Synology Power Control Bot")
- application = Application.builder().token(TELEGRAM_TOKEN).build()
-
- # Регистрация обработчиков команд
- application.add_handler(CommandHandler("start", start_command))
- application.add_handler(CommandHandler("help", help_command))
- application.add_handler(CommandHandler("status", status_command))
- application.add_handler(CommandHandler("power", power_command))
-
- # Регистрация расширенных обработчиков команд
- application.add_handler(CommandHandler("storage", storage_command))
- application.add_handler(CommandHandler("shares", shares_command))
- application.add_handler(CommandHandler("system", system_command))
- application.add_handler(CommandHandler("load", load_command))
- application.add_handler(CommandHandler("security", security_command))
- application.add_handler(CommandHandler("checkapi", check_api_command))
-
- # Регистрация продвинутых обработчиков команд
- application.add_handler(CommandHandler("processes", processes_command))
- application.add_handler(CommandHandler("network", network_command))
- application.add_handler(CommandHandler("temperature", temperature_command))
- application.add_handler(CommandHandler("schedule", schedule_command))
- application.add_handler(CommandHandler("browse", browse_command))
- application.add_handler(CommandHandler("search", search_command))
- application.add_handler(CommandHandler("updates", updates_command))
- application.add_handler(CommandHandler("backup", backup_command))
- application.add_handler(CommandHandler("quickreboot", quickreboot_command))
- application.add_handler(CommandHandler("reboot", reboot_command))
- application.add_handler(CommandHandler("sleep", sleep_command))
- application.add_handler(CommandHandler("wakeup", wakeup_command))
- application.add_handler(CommandHandler("quota", quota_command))
-
- # Регистрация обработчиков для управления администраторами
- application.add_handler(CommandHandler("addadmin", add_admin))
- application.add_handler(CommandHandler("removeadmin", remove_admin))
- application.add_handler(CommandHandler("admins", list_admins))
-
- # Регистрация обработчиков callback-запросов
- # Сначала обрабатываем более специфичные паттерны
- application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py
- # Затем более общие паттерны
- application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py
- application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_"))
- application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_"))
-
- # Создание экземпляра API и добавление методов для работы с файловой системой
- synology_api = SynologyAPI()
-
- # Регистрация обработчика файлового менеджера
- file_manager_handler = create_file_manager_handler(synology_api)
- application.add_handler(file_manager_handler)
-
- # Настройка обработчиков сигналов для корректного завершения
- signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application))
- signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application))
-
- # Запуск бота
- logger.info("Bot started. Press Ctrl+C to stop.")
- application.run_polling(allowed_updates=["message", "callback_query"])
-
-if __name__ == "__main__":
- main()
diff --git a/.history/src/config/config_20250830063519.py b/.history/src/config/config_20250830063519.py
deleted file mode 100644
index 70c1dbe..0000000
--- a/.history/src/config/config_20250830063519.py
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Конфигурационный файл для телеграм-бота управления Synology NAS
-"""
-
-import os
-from dotenv import load_dotenv
-
-# Загрузка переменных окружения из .env файла
-load_dotenv()
-
-# Конфигурация для Telegram бота
-TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
-ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else []
-
-# Конфигурация для Synology NAS
-SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST")
-SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000))
-SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME")
-SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD")
-SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true"
-SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10))
-
-# Конфигурация для Wake-on-LAN
-SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC")
-WOL_PORT = int(os.getenv("WOL_PORT", 9))
diff --git a/.history/src/config/config_20250830063839.py b/.history/src/config/config_20250830063839.py
deleted file mode 100644
index 70c1dbe..0000000
--- a/.history/src/config/config_20250830063839.py
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Конфигурационный файл для телеграм-бота управления Synology NAS
-"""
-
-import os
-from dotenv import load_dotenv
-
-# Загрузка переменных окружения из .env файла
-load_dotenv()
-
-# Конфигурация для Telegram бота
-TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
-ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else []
-
-# Конфигурация для Synology NAS
-SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST")
-SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000))
-SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME")
-SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD")
-SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true"
-SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10))
-
-# Конфигурация для Wake-on-LAN
-SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC")
-WOL_PORT = int(os.getenv("WOL_PORT", 9))
diff --git a/.history/src/config/config_20250830082127.py b/.history/src/config/config_20250830082127.py
deleted file mode 100644
index b81c052..0000000
--- a/.history/src/config/config_20250830082127.py
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Конфигурационный файл для телеграм-бота управления Synology NAS
-"""
-
-import os
-from dotenv import load_dotenv
-
-# Загрузка переменных окружения из .env файла
-load_dotenv()
-
-# Конфигурация для Telegram бота
-TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
-ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else []
-
-# Конфигурация для Synology NAS
-SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST")
-SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000))
-SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME")
-SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD")
-SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true"
-SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10))
-SYNOLOGY_API_VERSION = int(os.getenv("SYNOLOGY_API_VERSION", 1))
-
-# Конфигурация для Wake-on-LAN
-SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC")
-WOL_PORT = int(os.getenv("WOL_PORT", 9))
diff --git a/.history/src/config/config_20250830082144.py b/.history/src/config/config_20250830082144.py
deleted file mode 100644
index b81c052..0000000
--- a/.history/src/config/config_20250830082144.py
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Конфигурационный файл для телеграм-бота управления Synology NAS
-"""
-
-import os
-from dotenv import load_dotenv
-
-# Загрузка переменных окружения из .env файла
-load_dotenv()
-
-# Конфигурация для Telegram бота
-TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
-ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else []
-
-# Конфигурация для Synology NAS
-SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST")
-SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000))
-SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME")
-SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD")
-SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true"
-SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10))
-SYNOLOGY_API_VERSION = int(os.getenv("SYNOLOGY_API_VERSION", 1))
-
-# Конфигурация для Wake-on-LAN
-SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC")
-WOL_PORT = int(os.getenv("WOL_PORT", 9))
diff --git a/.history/src/config/config_20250830082223.py b/.history/src/config/config_20250830082223.py
deleted file mode 100644
index 79d954b..0000000
--- a/.history/src/config/config_20250830082223.py
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Конфигурационный файл для телеграм-бота управления Synology NAS
-"""
-
-import os
-from dotenv import load_dotenv
-
-# Загрузка переменных окружения из .env файла
-load_dotenv()
-
-# Конфигурация для Telegram бота
-TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
-ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else []
-
-# Конфигурация для Synology NAS
-SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST")
-SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000))
-SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME")
-SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD")
-SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true"
-SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10))
-SYNOLOGY_API_VERSION = int(os.getenv("SYNOLOGY_API_VERSION", 1))
-SYNOLOGY_POWER_API = os.getenv("SYNOLOGY_POWER_API", "SYNO.Core.Hardware.PowerRecovery")
-SYNOLOGY_INFO_API = os.getenv("SYNOLOGY_INFO_API", "SYNO.DSM.Info")
-
-# Конфигурация для Wake-on-LAN
-SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC")
-WOL_PORT = int(os.getenv("WOL_PORT", 9))
diff --git a/.history/src/config/config_20250830082500.py b/.history/src/config/config_20250830082500.py
deleted file mode 100644
index 79d954b..0000000
--- a/.history/src/config/config_20250830082500.py
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Конфигурационный файл для телеграм-бота управления Synology NAS
-"""
-
-import os
-from dotenv import load_dotenv
-
-# Загрузка переменных окружения из .env файла
-load_dotenv()
-
-# Конфигурация для Telegram бота
-TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
-ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else []
-
-# Конфигурация для Synology NAS
-SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST")
-SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000))
-SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME")
-SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD")
-SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true"
-SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10))
-SYNOLOGY_API_VERSION = int(os.getenv("SYNOLOGY_API_VERSION", 1))
-SYNOLOGY_POWER_API = os.getenv("SYNOLOGY_POWER_API", "SYNO.Core.Hardware.PowerRecovery")
-SYNOLOGY_INFO_API = os.getenv("SYNOLOGY_INFO_API", "SYNO.DSM.Info")
-
-# Конфигурация для Wake-on-LAN
-SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC")
-WOL_PORT = int(os.getenv("WOL_PORT", 9))
diff --git a/.history/src/config/config_20250830100958.py b/.history/src/config/config_20250830100958.py
deleted file mode 100644
index 17840bf..0000000
--- a/.history/src/config/config_20250830100958.py
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Конфигурационный файл для телеграм-бота управления Synology NAS
-"""
-
-import os
-from dotenv import load_dotenv
-
-# Загрузка переменных окружения из .env файла
-load_dotenv()
-
-# Конфигурация для Telegram бота
-TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
-ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else []
-
-# Конфигурация для Synology NAS
-SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST")
-SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000))
-SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME")
-SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD")
-SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true"
-SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10))
-SYNOLOGY_API_VERSION = int(os.getenv("SYNOLOGY_API_VERSION", 1))
-SYNOLOGY_POWER_API = os.getenv("SYNOLOGY_POWER_API", "SYNO.Core.System")
-SYNOLOGY_INFO_API = os.getenv("SYNOLOGY_INFO_API", "SYNO.DSM.Info")
-
-# Конфигурация для Wake-on-LAN
-SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC")
-WOL_PORT = int(os.getenv("WOL_PORT", 9))
diff --git a/.history/src/config/config_20250830101843.py b/.history/src/config/config_20250830101843.py
deleted file mode 100644
index 17840bf..0000000
--- a/.history/src/config/config_20250830101843.py
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Конфигурационный файл для телеграм-бота управления Synology NAS
-"""
-
-import os
-from dotenv import load_dotenv
-
-# Загрузка переменных окружения из .env файла
-load_dotenv()
-
-# Конфигурация для Telegram бота
-TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
-ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else []
-
-# Конфигурация для Synology NAS
-SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST")
-SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000))
-SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME")
-SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD")
-SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true"
-SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10))
-SYNOLOGY_API_VERSION = int(os.getenv("SYNOLOGY_API_VERSION", 1))
-SYNOLOGY_POWER_API = os.getenv("SYNOLOGY_POWER_API", "SYNO.Core.System")
-SYNOLOGY_INFO_API = os.getenv("SYNOLOGY_INFO_API", "SYNO.DSM.Info")
-
-# Конфигурация для Wake-on-LAN
-SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC")
-WOL_PORT = int(os.getenv("WOL_PORT", 9))
diff --git a/.history/src/handlers/advanced_handlers_20250830091501.py b/.history/src/handlers/advanced_handlers_20250830091501.py
deleted file mode 100644
index 02cef9d..0000000
--- a/.history/src/handlers/advanced_handlers_20250830091501.py
+++ /dev/null
@@ -1,864 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Расширенные обработчики команд для управления Synology NAS
-"""
-
-import logging
-from datetime import datetime
-from typing import List, Dict, Any
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /processes для получения списка активных процессов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
- return
-
- try:
- processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
-
- if not processes:
- await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о процессах
- reply_text = f"⚙️ Активные процессы Synology NAS\n\n"
-
- for process in processes:
- name = process.get("name", "unknown")
- pid = process.get("pid", "?")
- cpu_usage = process.get("cpu_usage", 0)
- memory_usage = process.get("memory_usage", 0)
-
- reply_text += f"• {name} (PID: {pid})\n"
- reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
-
- reply_text += f"\nПоказано {len(processes)} наиболее активных процессов"
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /network для получения информации о сетевых подключениях"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
- return
-
- try:
- network_status = synology_api.get_network_status()
-
- if not network_status:
- await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о сетевых интерфейсах
- interfaces = network_status.get("interfaces", [])
-
- reply_text = f"🌐 Сетевые подключения Synology NAS\n\n"
-
- for interface in interfaces:
- name = interface.get("id", "unknown")
- ip = interface.get("ip", "Нет данных")
- mac = interface.get("mac", "Нет данных")
- status = "Активен" if interface.get("status") else "Неактивен"
-
- # Информация о трафике
- rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
- tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- if rx_bytes > 0 or tx_bytes > 0:
- reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /temperature для мониторинга температуры"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о температуре...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
- return
-
- try:
- temp_status = synology_api.get_temperature_status()
-
- if not temp_status:
- await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о температуре
- system_temp = temp_status.get("system_temperature")
- disk_temps = temp_status.get("disk_temperatures", [])
- is_warning = temp_status.get("warning", False)
-
- # Выбор emoji в зависимости от температуры
- temp_emoji = "🔥" if is_warning else "🌡️"
-
- reply_text = f"{temp_emoji} Температура Synology NAS\n\n"
-
- if system_temp is not None:
- temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная"
- reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n"
-
- if disk_temps:
- reply_text += "Температура дисков:\n"
- for disk in disk_temps:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temperature", 0)
-
- disk_temp_emoji = "🔥" if temp > 45 else "✅"
- reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /schedule для управления расписанием питания"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
- return
-
- try:
- schedule = synology_api.get_power_schedule()
-
- if not schedule:
- await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о расписании питания
- boot_tasks = schedule.get("boot_tasks", [])
- shutdown_tasks = schedule.get("shutdown_tasks", [])
-
- reply_text = f"⏱️ Расписание питания Synology NAS\n\n"
-
- if boot_tasks:
- reply_text += "Расписание включения:\n"
- for task in boot_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание включения: Не настроено\n"
-
- reply_text += "\n"
-
- if shutdown_tasks:
- reply_text += "Расписание выключения:\n"
- for task in shutdown_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание выключения: Не настроено\n"
-
- # Добавляем кнопки для управления расписанием
- keyboard = [
- [
- InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"),
- InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown")
- ],
- [
- InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
-
-async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /browse для просмотра файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем путь из аргументов команды или используем корневую директорию
- path = " ".join(context.args) if context.args else ""
-
- message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /search для поиска файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем шаблон поиска из аргументов команды
- if not context.args:
- await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>")
- return
-
- pattern = " ".join(context.args)
-
- message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
- return
-
- try:
- search_result = synology_api.search_files(pattern=pattern, limit=20)
-
- if not search_result.get("success", False):
- error = search_result.get("error", "unknown")
- progress = search_result.get("progress", 0)
-
- if error == "search_timeout":
- await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
- else:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- files = search_result.get("results", [])
- total = search_result.get("total", len(files))
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение с результатами поиска
- reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n"
-
- if not files:
- reply_text += "📭 Файлы не найдены"
- else:
- # Сортируем: сначала папки, потом файлы
- folders = []
- found_files = []
-
- for item in files:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path))
- else:
- # Для файлов получаем размер и путь к родительской папке
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- parent_path = "/".join(path.split("/")[:-1])
- found_files.append((name, path, size_str, parent_path))
-
- # Добавляем папки в сообщение
- if folders:
- reply_text += "Найденные папки:\n"
- for name, path in folders[:5]: # Показываем первые 5 папок
- reply_text += f"📁 {name}\n"
-
- if len(folders) > 5:
- reply_text += f"...и еще {len(folders) - 5} папок\n"
-
- reply_text += "\n"
-
- # Добавляем файлы в сообщение
- if found_files:
- reply_text += "Найденные файлы:\n"
- for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
- reply_text += f" Путь: .../{path.split('/')[-2]}/\n"
-
- if len(found_files) > 10:
- reply_text += f"...и еще {len(found_files) - 10} файлов\n"
-
- # Добавляем информацию о общем количестве результатов
- reply_text += f"\nВсего найдено: {total} элементов"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /updates для проверки обновлений"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
- return
-
- try:
- update_info = synology_api.check_for_updates()
-
- if not update_info.get("success", False):
- error = update_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML")
- return
-
- current_version = update_info.get("current_version", "unknown")
- update_available = update_info.get("update_available", False)
- auto_update = update_info.get("auto_update_enabled", False)
- updates = update_info.get("updates", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об обновлениях
- if update_available:
- reply_text = f"🔄 Доступны обновления DSM\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
- reply_text += "Доступные обновления:\n"
-
- for update_item in updates:
- update_name = update_item.get("name", "unknown")
- update_version = update_item.get("version", "unknown")
- update_size = update_item.get("size", 0)
- update_size_str = format_size(update_size)
-
- reply_text += f"• {update_name} v{update_version}\n"
- reply_text += f" └ Размер: {update_size_str}\n"
- else:
- reply_text = f"✅ Система в актуальном состоянии\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /backup для управления резервным копированием"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
- return
-
- try:
- backup_status = synology_api.get_backup_status()
-
- if not backup_status.get("success", False):
- error = backup_status.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML")
- return
-
- backups = backup_status.get("backups", {})
- api_status = backup_status.get("available_apis", {})
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о резервном копировании
- reply_text = f"💾 Резервное копирование Synology NAS\n\n"
-
- # Информация о Hyper Backup
- hyper_backups = backups.get("hyper_backup", [])
- hyper_api_available = api_status.get("hyper_backup", False)
-
- if hyper_api_available:
- reply_text += "Hyper Backup:\n"
-
- if hyper_backups:
- for backup in hyper_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
- last_backup = backup.get("last_backup", "never")
-
- status_emoji = "✅" if status.lower() == "success" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- reply_text += f" └ Последнее копирование: {last_backup}\n"
- else:
- reply_text += "Задачи Hyper Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о Time Backup
- time_backups = backups.get("time_backup", [])
- time_api_available = api_status.get("time_backup", False)
-
- if time_api_available:
- reply_text += "Time Backup:\n"
-
- if time_backups:
- for backup in time_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
-
- status_emoji = "✅" if status.lower() == "normal" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- else:
- reply_text += "Задачи Time Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о USB Copy
- usb_copy = backups.get("usb_copy", {})
- usb_api_available = api_status.get("usb_copy", False)
-
- if usb_api_available:
- usb_enabled = usb_copy.get("enabled", False)
- usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
-
- reply_text += f"USB Copy: {usb_status}\n\n"
-
- # Если ни один из API не доступен
- if not any(api_status.values()):
- reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- # Выполняем перезагрузку
- result = synology_api.reboot_system()
-
- if result:
- # Формируем сообщение об успешной перезагрузке
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /wakeup для включения NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
-
- # Проверяем, не включен ли NAS уже
- if synology_api.is_online(force_check=True):
- await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
- return
-
- try:
- # Отправляем сигнал пробуждения
- result = synology_api.power_on()
-
- if result:
- # Формируем сообщение об успешном включении
- reply_text = "✅ Synology NAS успешно включен\n\n"
- reply_text += "NAS полностью готов к работе."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quota для просмотра информации о квотах"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
- return
-
- try:
- quota_info = synology_api.get_quota_info()
-
- if not quota_info.get("success", False):
- error = quota_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML")
- return
-
- user_quotas = quota_info.get("user_quotas", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о квотах
- reply_text = f"📊 Квоты пользователей Synology NAS\n\n"
-
- if not user_quotas:
- reply_text += "Квоты пользователей не настроены или недоступны"
- else:
- for user_quota in user_quotas:
- user = user_quota.get("user", "unknown")
- quotas = user_quota.get("quotas", [])
-
- if quotas:
- reply_text += f"Пользователь {user}:\n"
-
- for quota in quotas:
- volume = quota.get("volume_name", "unknown")
- limit = quota.get("limit", 0)
- used = quota.get("used", 0)
-
- # Переводим байты в ГБ
- limit_gb = limit / (1024**3) if limit > 0 else 0
- used_gb = used / (1024**3)
-
- # Рассчитываем процент использования
- if limit_gb > 0:
- usage_percent = (used_gb / limit_gb) * 100
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- else:
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
-
- reply_text += "\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления расписанием питания"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("schedule_"):
- action_type = action.split("_")[1]
-
- if action_type == "add_boot":
- # Логика добавления расписания включения
- # В реальном боте здесь будет диалог для настройки расписания
- await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "add_shutdown":
- # Логика добавления расписания выключения
- await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "delete":
- # Логика удаления расписания
- await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
-async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для навигации по файловой системе"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("browse_"):
- path = action[7:] # Убираем префикс "browse_"
-
- # Используем команду browse с указанным путем
- message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках (аналогично функции browse_command)
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-# Вспомогательные функции
-
-def format_size(size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат"""
- if size_bytes < 1024:
- return f"{size_bytes} Б"
- elif size_bytes < 1024**2:
- return f"{size_bytes/1024:.1f} КБ"
- elif size_bytes < 1024**3:
- return f"{size_bytes/1024**2:.1f} МБ"
- else:
- return f"{size_bytes/1024**3:.1f} ГБ"
-
-def get_file_icon(filename: str) -> str:
- """Возвращает эмодзи-иконку в зависимости от типа файла"""
- extension = filename.lower().split('.')[-1] if '.' in filename else ''
-
- if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
- return "🖼️"
- elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
- return "🎬"
- elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
- return "🎵"
- elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
- return "📄"
- elif extension in ['xls', 'xlsx', 'csv']:
- return "📊"
- elif extension in ['ppt', 'pptx']:
- return "📑"
- elif extension in ['pdf']:
- return "📕"
- elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
- return "🗜️"
- elif extension in ['exe', 'msi']:
- return "⚙️"
- else:
- return "📄"
diff --git a/.history/src/handlers/advanced_handlers_20250830092441.py b/.history/src/handlers/advanced_handlers_20250830092441.py
deleted file mode 100644
index 02cef9d..0000000
--- a/.history/src/handlers/advanced_handlers_20250830092441.py
+++ /dev/null
@@ -1,864 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Расширенные обработчики команд для управления Synology NAS
-"""
-
-import logging
-from datetime import datetime
-from typing import List, Dict, Any
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /processes для получения списка активных процессов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
- return
-
- try:
- processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
-
- if not processes:
- await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о процессах
- reply_text = f"⚙️ Активные процессы Synology NAS\n\n"
-
- for process in processes:
- name = process.get("name", "unknown")
- pid = process.get("pid", "?")
- cpu_usage = process.get("cpu_usage", 0)
- memory_usage = process.get("memory_usage", 0)
-
- reply_text += f"• {name} (PID: {pid})\n"
- reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
-
- reply_text += f"\nПоказано {len(processes)} наиболее активных процессов"
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /network для получения информации о сетевых подключениях"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
- return
-
- try:
- network_status = synology_api.get_network_status()
-
- if not network_status:
- await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о сетевых интерфейсах
- interfaces = network_status.get("interfaces", [])
-
- reply_text = f"🌐 Сетевые подключения Synology NAS\n\n"
-
- for interface in interfaces:
- name = interface.get("id", "unknown")
- ip = interface.get("ip", "Нет данных")
- mac = interface.get("mac", "Нет данных")
- status = "Активен" if interface.get("status") else "Неактивен"
-
- # Информация о трафике
- rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
- tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- if rx_bytes > 0 or tx_bytes > 0:
- reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /temperature для мониторинга температуры"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о температуре...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
- return
-
- try:
- temp_status = synology_api.get_temperature_status()
-
- if not temp_status:
- await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о температуре
- system_temp = temp_status.get("system_temperature")
- disk_temps = temp_status.get("disk_temperatures", [])
- is_warning = temp_status.get("warning", False)
-
- # Выбор emoji в зависимости от температуры
- temp_emoji = "🔥" if is_warning else "🌡️"
-
- reply_text = f"{temp_emoji} Температура Synology NAS\n\n"
-
- if system_temp is not None:
- temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная"
- reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n"
-
- if disk_temps:
- reply_text += "Температура дисков:\n"
- for disk in disk_temps:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temperature", 0)
-
- disk_temp_emoji = "🔥" if temp > 45 else "✅"
- reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /schedule для управления расписанием питания"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
- return
-
- try:
- schedule = synology_api.get_power_schedule()
-
- if not schedule:
- await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о расписании питания
- boot_tasks = schedule.get("boot_tasks", [])
- shutdown_tasks = schedule.get("shutdown_tasks", [])
-
- reply_text = f"⏱️ Расписание питания Synology NAS\n\n"
-
- if boot_tasks:
- reply_text += "Расписание включения:\n"
- for task in boot_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание включения: Не настроено\n"
-
- reply_text += "\n"
-
- if shutdown_tasks:
- reply_text += "Расписание выключения:\n"
- for task in shutdown_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание выключения: Не настроено\n"
-
- # Добавляем кнопки для управления расписанием
- keyboard = [
- [
- InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"),
- InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown")
- ],
- [
- InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
-
-async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /browse для просмотра файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем путь из аргументов команды или используем корневую директорию
- path = " ".join(context.args) if context.args else ""
-
- message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /search для поиска файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем шаблон поиска из аргументов команды
- if not context.args:
- await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>")
- return
-
- pattern = " ".join(context.args)
-
- message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
- return
-
- try:
- search_result = synology_api.search_files(pattern=pattern, limit=20)
-
- if not search_result.get("success", False):
- error = search_result.get("error", "unknown")
- progress = search_result.get("progress", 0)
-
- if error == "search_timeout":
- await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
- else:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- files = search_result.get("results", [])
- total = search_result.get("total", len(files))
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение с результатами поиска
- reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n"
-
- if not files:
- reply_text += "📭 Файлы не найдены"
- else:
- # Сортируем: сначала папки, потом файлы
- folders = []
- found_files = []
-
- for item in files:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path))
- else:
- # Для файлов получаем размер и путь к родительской папке
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- parent_path = "/".join(path.split("/")[:-1])
- found_files.append((name, path, size_str, parent_path))
-
- # Добавляем папки в сообщение
- if folders:
- reply_text += "Найденные папки:\n"
- for name, path in folders[:5]: # Показываем первые 5 папок
- reply_text += f"📁 {name}\n"
-
- if len(folders) > 5:
- reply_text += f"...и еще {len(folders) - 5} папок\n"
-
- reply_text += "\n"
-
- # Добавляем файлы в сообщение
- if found_files:
- reply_text += "Найденные файлы:\n"
- for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
- reply_text += f" Путь: .../{path.split('/')[-2]}/\n"
-
- if len(found_files) > 10:
- reply_text += f"...и еще {len(found_files) - 10} файлов\n"
-
- # Добавляем информацию о общем количестве результатов
- reply_text += f"\nВсего найдено: {total} элементов"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /updates для проверки обновлений"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
- return
-
- try:
- update_info = synology_api.check_for_updates()
-
- if not update_info.get("success", False):
- error = update_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML")
- return
-
- current_version = update_info.get("current_version", "unknown")
- update_available = update_info.get("update_available", False)
- auto_update = update_info.get("auto_update_enabled", False)
- updates = update_info.get("updates", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об обновлениях
- if update_available:
- reply_text = f"🔄 Доступны обновления DSM\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
- reply_text += "Доступные обновления:\n"
-
- for update_item in updates:
- update_name = update_item.get("name", "unknown")
- update_version = update_item.get("version", "unknown")
- update_size = update_item.get("size", 0)
- update_size_str = format_size(update_size)
-
- reply_text += f"• {update_name} v{update_version}\n"
- reply_text += f" └ Размер: {update_size_str}\n"
- else:
- reply_text = f"✅ Система в актуальном состоянии\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /backup для управления резервным копированием"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
- return
-
- try:
- backup_status = synology_api.get_backup_status()
-
- if not backup_status.get("success", False):
- error = backup_status.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML")
- return
-
- backups = backup_status.get("backups", {})
- api_status = backup_status.get("available_apis", {})
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о резервном копировании
- reply_text = f"💾 Резервное копирование Synology NAS\n\n"
-
- # Информация о Hyper Backup
- hyper_backups = backups.get("hyper_backup", [])
- hyper_api_available = api_status.get("hyper_backup", False)
-
- if hyper_api_available:
- reply_text += "Hyper Backup:\n"
-
- if hyper_backups:
- for backup in hyper_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
- last_backup = backup.get("last_backup", "never")
-
- status_emoji = "✅" if status.lower() == "success" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- reply_text += f" └ Последнее копирование: {last_backup}\n"
- else:
- reply_text += "Задачи Hyper Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о Time Backup
- time_backups = backups.get("time_backup", [])
- time_api_available = api_status.get("time_backup", False)
-
- if time_api_available:
- reply_text += "Time Backup:\n"
-
- if time_backups:
- for backup in time_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
-
- status_emoji = "✅" if status.lower() == "normal" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- else:
- reply_text += "Задачи Time Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о USB Copy
- usb_copy = backups.get("usb_copy", {})
- usb_api_available = api_status.get("usb_copy", False)
-
- if usb_api_available:
- usb_enabled = usb_copy.get("enabled", False)
- usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
-
- reply_text += f"USB Copy: {usb_status}\n\n"
-
- # Если ни один из API не доступен
- if not any(api_status.values()):
- reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- # Выполняем перезагрузку
- result = synology_api.reboot_system()
-
- if result:
- # Формируем сообщение об успешной перезагрузке
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /wakeup для включения NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
-
- # Проверяем, не включен ли NAS уже
- if synology_api.is_online(force_check=True):
- await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
- return
-
- try:
- # Отправляем сигнал пробуждения
- result = synology_api.power_on()
-
- if result:
- # Формируем сообщение об успешном включении
- reply_text = "✅ Synology NAS успешно включен\n\n"
- reply_text += "NAS полностью готов к работе."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quota для просмотра информации о квотах"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
- return
-
- try:
- quota_info = synology_api.get_quota_info()
-
- if not quota_info.get("success", False):
- error = quota_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML")
- return
-
- user_quotas = quota_info.get("user_quotas", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о квотах
- reply_text = f"📊 Квоты пользователей Synology NAS\n\n"
-
- if not user_quotas:
- reply_text += "Квоты пользователей не настроены или недоступны"
- else:
- for user_quota in user_quotas:
- user = user_quota.get("user", "unknown")
- quotas = user_quota.get("quotas", [])
-
- if quotas:
- reply_text += f"Пользователь {user}:\n"
-
- for quota in quotas:
- volume = quota.get("volume_name", "unknown")
- limit = quota.get("limit", 0)
- used = quota.get("used", 0)
-
- # Переводим байты в ГБ
- limit_gb = limit / (1024**3) if limit > 0 else 0
- used_gb = used / (1024**3)
-
- # Рассчитываем процент использования
- if limit_gb > 0:
- usage_percent = (used_gb / limit_gb) * 100
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- else:
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
-
- reply_text += "\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления расписанием питания"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("schedule_"):
- action_type = action.split("_")[1]
-
- if action_type == "add_boot":
- # Логика добавления расписания включения
- # В реальном боте здесь будет диалог для настройки расписания
- await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "add_shutdown":
- # Логика добавления расписания выключения
- await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "delete":
- # Логика удаления расписания
- await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
-async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для навигации по файловой системе"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("browse_"):
- path = action[7:] # Убираем префикс "browse_"
-
- # Используем команду browse с указанным путем
- message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках (аналогично функции browse_command)
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-# Вспомогательные функции
-
-def format_size(size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат"""
- if size_bytes < 1024:
- return f"{size_bytes} Б"
- elif size_bytes < 1024**2:
- return f"{size_bytes/1024:.1f} КБ"
- elif size_bytes < 1024**3:
- return f"{size_bytes/1024**2:.1f} МБ"
- else:
- return f"{size_bytes/1024**3:.1f} ГБ"
-
-def get_file_icon(filename: str) -> str:
- """Возвращает эмодзи-иконку в зависимости от типа файла"""
- extension = filename.lower().split('.')[-1] if '.' in filename else ''
-
- if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
- return "🖼️"
- elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
- return "🎬"
- elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
- return "🎵"
- elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
- return "📄"
- elif extension in ['xls', 'xlsx', 'csv']:
- return "📊"
- elif extension in ['ppt', 'pptx']:
- return "📑"
- elif extension in ['pdf']:
- return "📕"
- elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
- return "🗜️"
- elif extension in ['exe', 'msi']:
- return "⚙️"
- else:
- return "📄"
diff --git a/.history/src/handlers/advanced_handlers_20250830093327.py b/.history/src/handlers/advanced_handlers_20250830093327.py
deleted file mode 100644
index e6b8d9b..0000000
--- a/.history/src/handlers/advanced_handlers_20250830093327.py
+++ /dev/null
@@ -1,912 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Расширенные обработчики команд для управления Synology NAS
-"""
-
-import logging
-from datetime import datetime
-from typing import List, Dict, Any
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /processes для получения списка активных процессов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
- return
-
- try:
- processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
-
- if not processes:
- await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о процессах
- reply_text = f"⚙️ Активные процессы Synology NAS\n\n"
-
- for process in processes:
- name = process.get("name", "unknown")
- pid = process.get("pid", "?")
- cpu_usage = process.get("cpu_usage", 0)
- memory_usage = process.get("memory_usage", 0)
-
- reply_text += f"• {name} (PID: {pid})\n"
- reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
-
- reply_text += f"\nПоказано {len(processes)} наиболее активных процессов"
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /network для получения информации о сетевых подключениях"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
- return
-
- try:
- network_status = synology_api.get_network_status()
-
- if not network_status:
- await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о сетевых интерфейсах
- interfaces = network_status.get("interfaces", [])
-
- reply_text = f"🌐 Сетевые подключения Synology NAS\n\n"
-
- for interface in interfaces:
- name = interface.get("id", "unknown")
- ip = interface.get("ip", "Нет данных")
- mac = interface.get("mac", "Нет данных")
- status = "Активен" if interface.get("status") else "Неактивен"
-
- # Информация о трафике
- rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
- tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- if rx_bytes > 0 or tx_bytes > 0:
- reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /temperature для мониторинга температуры"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о температуре...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
- return
-
- try:
- temp_status = synology_api.get_temperature_status()
-
- if not temp_status:
- await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о температуре
- system_temp = temp_status.get("system_temperature")
- disk_temps = temp_status.get("disk_temperatures", [])
- is_warning = temp_status.get("warning", False)
-
- # Выбор emoji в зависимости от температуры
- temp_emoji = "🔥" if is_warning else "🌡️"
-
- reply_text = f"{temp_emoji} Температура Synology NAS\n\n"
-
- if system_temp is not None:
- temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная"
- reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n"
-
- if disk_temps:
- reply_text += "Температура дисков:\n"
- for disk in disk_temps:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temperature", 0)
-
- disk_temp_emoji = "🔥" if temp > 45 else "✅"
- reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /schedule для управления расписанием питания"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
- return
-
- try:
- schedule = synology_api.get_power_schedule()
-
- if not schedule:
- await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о расписании питания
- boot_tasks = schedule.get("boot_tasks", [])
- shutdown_tasks = schedule.get("shutdown_tasks", [])
-
- reply_text = f"⏱️ Расписание питания Synology NAS\n\n"
-
- if boot_tasks:
- reply_text += "Расписание включения:\n"
- for task in boot_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание включения: Не настроено\n"
-
- reply_text += "\n"
-
- if shutdown_tasks:
- reply_text += "Расписание выключения:\n"
- for task in shutdown_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание выключения: Не настроено\n"
-
- # Добавляем кнопки для управления расписанием
- keyboard = [
- [
- InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"),
- InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown")
- ],
- [
- InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
-
-async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /browse для просмотра файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем путь из аргументов команды или используем корневую директорию
- path = " ".join(context.args) if context.args else ""
-
- message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /search для поиска файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем шаблон поиска из аргументов команды
- if not context.args:
- await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>")
- return
-
- pattern = " ".join(context.args)
-
- message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
- return
-
- try:
- search_result = synology_api.search_files(pattern=pattern, limit=20)
-
- if not search_result.get("success", False):
- error = search_result.get("error", "unknown")
- progress = search_result.get("progress", 0)
-
- if error == "search_timeout":
- await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
- else:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- files = search_result.get("results", [])
- total = search_result.get("total", len(files))
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение с результатами поиска
- reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n"
-
- if not files:
- reply_text += "📭 Файлы не найдены"
- else:
- # Сортируем: сначала папки, потом файлы
- folders = []
- found_files = []
-
- for item in files:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path))
- else:
- # Для файлов получаем размер и путь к родительской папке
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- parent_path = "/".join(path.split("/")[:-1])
- found_files.append((name, path, size_str, parent_path))
-
- # Добавляем папки в сообщение
- if folders:
- reply_text += "Найденные папки:\n"
- for name, path in folders[:5]: # Показываем первые 5 папок
- reply_text += f"📁 {name}\n"
-
- if len(folders) > 5:
- reply_text += f"...и еще {len(folders) - 5} папок\n"
-
- reply_text += "\n"
-
- # Добавляем файлы в сообщение
- if found_files:
- reply_text += "Найденные файлы:\n"
- for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
- reply_text += f" Путь: .../{path.split('/')[-2]}/\n"
-
- if len(found_files) > 10:
- reply_text += f"...и еще {len(found_files) - 10} файлов\n"
-
- # Добавляем информацию о общем количестве результатов
- reply_text += f"\nВсего найдено: {total} элементов"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /updates для проверки обновлений"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
- return
-
- try:
- update_info = synology_api.check_for_updates()
-
- if not update_info.get("success", False):
- error = update_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML")
- return
-
- current_version = update_info.get("current_version", "unknown")
- update_available = update_info.get("update_available", False)
- auto_update = update_info.get("auto_update_enabled", False)
- updates = update_info.get("updates", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об обновлениях
- if update_available:
- reply_text = f"🔄 Доступны обновления DSM\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
- reply_text += "Доступные обновления:\n"
-
- for update_item in updates:
- update_name = update_item.get("name", "unknown")
- update_version = update_item.get("version", "unknown")
- update_size = update_item.get("size", 0)
- update_size_str = format_size(update_size)
-
- reply_text += f"• {update_name} v{update_version}\n"
- reply_text += f" └ Размер: {update_size_str}\n"
- else:
- reply_text = f"✅ Система в актуальном состоянии\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /backup для управления резервным копированием"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
- return
-
- try:
- backup_status = synology_api.get_backup_status()
-
- if not backup_status.get("success", False):
- error = backup_status.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML")
- return
-
- backups = backup_status.get("backups", {})
- api_status = backup_status.get("available_apis", {})
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о резервном копировании
- reply_text = f"💾 Резервное копирование Synology NAS\n\n"
-
- # Информация о Hyper Backup
- hyper_backups = backups.get("hyper_backup", [])
- hyper_api_available = api_status.get("hyper_backup", False)
-
- if hyper_api_available:
- reply_text += "Hyper Backup:\n"
-
- if hyper_backups:
- for backup in hyper_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
- last_backup = backup.get("last_backup", "never")
-
- status_emoji = "✅" if status.lower() == "success" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- reply_text += f" └ Последнее копирование: {last_backup}\n"
- else:
- reply_text += "Задачи Hyper Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о Time Backup
- time_backups = backups.get("time_backup", [])
- time_api_available = api_status.get("time_backup", False)
-
- if time_api_available:
- reply_text += "Time Backup:\n"
-
- if time_backups:
- for backup in time_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
-
- status_emoji = "✅" if status.lower() == "normal" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- else:
- reply_text += "Задачи Time Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о USB Copy
- usb_copy = backups.get("usb_copy", {})
- usb_api_available = api_status.get("usb_copy", False)
-
- if usb_api_available:
- usb_enabled = usb_copy.get("enabled", False)
- usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
-
- reply_text += f"USB Copy: {usb_status}\n\n"
-
- # Если ни один из API не доступен
- if not any(api_status.values()):
- reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /reboot для перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед перезагрузкой
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n"
- "Это действие может привести к прерыванию работы всех сервисов.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /sleep для перевода NAS в спящий режим"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед отправкой в спящий режим
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n"
- "Это действие приведет к остановке всех сервисов и отключению NAS.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- # Выполняем перезагрузку
- result = synology_api.reboot_system()
-
- if result:
- # Формируем сообщение об успешной перезагрузке
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /wakeup для включения NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
-
- # Проверяем, не включен ли NAS уже
- if synology_api.is_online(force_check=True):
- await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
- return
-
- try:
- # Отправляем сигнал пробуждения
- result = synology_api.power_on()
-
- if result:
- # Формируем сообщение об успешном включении
- reply_text = "✅ Synology NAS успешно включен\n\n"
- reply_text += "NAS полностью готов к работе."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quota для просмотра информации о квотах"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
- return
-
- try:
- quota_info = synology_api.get_quota_info()
-
- if not quota_info.get("success", False):
- error = quota_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML")
- return
-
- user_quotas = quota_info.get("user_quotas", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о квотах
- reply_text = f"📊 Квоты пользователей Synology NAS\n\n"
-
- if not user_quotas:
- reply_text += "Квоты пользователей не настроены или недоступны"
- else:
- for user_quota in user_quotas:
- user = user_quota.get("user", "unknown")
- quotas = user_quota.get("quotas", [])
-
- if quotas:
- reply_text += f"Пользователь {user}:\n"
-
- for quota in quotas:
- volume = quota.get("volume_name", "unknown")
- limit = quota.get("limit", 0)
- used = quota.get("used", 0)
-
- # Переводим байты в ГБ
- limit_gb = limit / (1024**3) if limit > 0 else 0
- used_gb = used / (1024**3)
-
- # Рассчитываем процент использования
- if limit_gb > 0:
- usage_percent = (used_gb / limit_gb) * 100
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- else:
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
-
- reply_text += "\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления расписанием питания"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("schedule_"):
- action_type = action.split("_")[1]
-
- if action_type == "add_boot":
- # Логика добавления расписания включения
- # В реальном боте здесь будет диалог для настройки расписания
- await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "add_shutdown":
- # Логика добавления расписания выключения
- await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "delete":
- # Логика удаления расписания
- await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
-async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для навигации по файловой системе"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("browse_"):
- path = action[7:] # Убираем префикс "browse_"
-
- # Используем команду browse с указанным путем
- message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках (аналогично функции browse_command)
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-# Вспомогательные функции
-
-def format_size(size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат"""
- if size_bytes < 1024:
- return f"{size_bytes} Б"
- elif size_bytes < 1024**2:
- return f"{size_bytes/1024:.1f} КБ"
- elif size_bytes < 1024**3:
- return f"{size_bytes/1024**2:.1f} МБ"
- else:
- return f"{size_bytes/1024**3:.1f} ГБ"
-
-def get_file_icon(filename: str) -> str:
- """Возвращает эмодзи-иконку в зависимости от типа файла"""
- extension = filename.lower().split('.')[-1] if '.' in filename else ''
-
- if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
- return "🖼️"
- elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
- return "🎬"
- elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
- return "🎵"
- elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
- return "📄"
- elif extension in ['xls', 'xlsx', 'csv']:
- return "📊"
- elif extension in ['ppt', 'pptx']:
- return "📑"
- elif extension in ['pdf']:
- return "📕"
- elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
- return "🗜️"
- elif extension in ['exe', 'msi']:
- return "⚙️"
- else:
- return "📄"
diff --git a/.history/src/handlers/advanced_handlers_20250830093424.py b/.history/src/handlers/advanced_handlers_20250830093424.py
deleted file mode 100644
index a7493f2..0000000
--- a/.history/src/handlers/advanced_handlers_20250830093424.py
+++ /dev/null
@@ -1,972 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Расширенные обработчики команд для управления Synology NAS
-"""
-
-import logging
-from datetime import datetime
-from typing import List, Dict, Any
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /processes для получения списка активных процессов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
- return
-
- try:
- processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
-
- if not processes:
- await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о процессах
- reply_text = f"⚙️ Активные процессы Synology NAS\n\n"
-
- for process in processes:
- name = process.get("name", "unknown")
- pid = process.get("pid", "?")
- cpu_usage = process.get("cpu_usage", 0)
- memory_usage = process.get("memory_usage", 0)
-
- reply_text += f"• {name} (PID: {pid})\n"
- reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
-
- reply_text += f"\nПоказано {len(processes)} наиболее активных процессов"
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /network для получения информации о сетевых подключениях"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
- return
-
- try:
- network_status = synology_api.get_network_status()
-
- if not network_status:
- await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о сетевых интерфейсах
- interfaces = network_status.get("interfaces", [])
-
- reply_text = f"🌐 Сетевые подключения Synology NAS\n\n"
-
- for interface in interfaces:
- name = interface.get("id", "unknown")
- ip = interface.get("ip", "Нет данных")
- mac = interface.get("mac", "Нет данных")
- status = "Активен" if interface.get("status") else "Неактивен"
-
- # Информация о трафике
- rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
- tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- if rx_bytes > 0 or tx_bytes > 0:
- reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /temperature для мониторинга температуры"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о температуре...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
- return
-
- try:
- temp_status = synology_api.get_temperature_status()
-
- if not temp_status:
- await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о температуре
- system_temp = temp_status.get("system_temperature")
- disk_temps = temp_status.get("disk_temperatures", [])
- is_warning = temp_status.get("warning", False)
-
- # Выбор emoji в зависимости от температуры
- temp_emoji = "🔥" if is_warning else "🌡️"
-
- reply_text = f"{temp_emoji} Температура Synology NAS\n\n"
-
- if system_temp is not None:
- temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная"
- reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n"
-
- if disk_temps:
- reply_text += "Температура дисков:\n"
- for disk in disk_temps:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temperature", 0)
-
- disk_temp_emoji = "🔥" if temp > 45 else "✅"
- reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /schedule для управления расписанием питания"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
- return
-
- try:
- schedule = synology_api.get_power_schedule()
-
- if not schedule:
- await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о расписании питания
- boot_tasks = schedule.get("boot_tasks", [])
- shutdown_tasks = schedule.get("shutdown_tasks", [])
-
- reply_text = f"⏱️ Расписание питания Synology NAS\n\n"
-
- if boot_tasks:
- reply_text += "Расписание включения:\n"
- for task in boot_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание включения: Не настроено\n"
-
- reply_text += "\n"
-
- if shutdown_tasks:
- reply_text += "Расписание выключения:\n"
- for task in shutdown_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание выключения: Не настроено\n"
-
- # Добавляем кнопки для управления расписанием
- keyboard = [
- [
- InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"),
- InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown")
- ],
- [
- InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
-
-async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /browse для просмотра файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем путь из аргументов команды или используем корневую директорию
- path = " ".join(context.args) if context.args else ""
-
- message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /search для поиска файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем шаблон поиска из аргументов команды
- if not context.args:
- await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>")
- return
-
- pattern = " ".join(context.args)
-
- message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
- return
-
- try:
- search_result = synology_api.search_files(pattern=pattern, limit=20)
-
- if not search_result.get("success", False):
- error = search_result.get("error", "unknown")
- progress = search_result.get("progress", 0)
-
- if error == "search_timeout":
- await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
- else:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- files = search_result.get("results", [])
- total = search_result.get("total", len(files))
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение с результатами поиска
- reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n"
-
- if not files:
- reply_text += "📭 Файлы не найдены"
- else:
- # Сортируем: сначала папки, потом файлы
- folders = []
- found_files = []
-
- for item in files:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path))
- else:
- # Для файлов получаем размер и путь к родительской папке
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- parent_path = "/".join(path.split("/")[:-1])
- found_files.append((name, path, size_str, parent_path))
-
- # Добавляем папки в сообщение
- if folders:
- reply_text += "Найденные папки:\n"
- for name, path in folders[:5]: # Показываем первые 5 папок
- reply_text += f"📁 {name}\n"
-
- if len(folders) > 5:
- reply_text += f"...и еще {len(folders) - 5} папок\n"
-
- reply_text += "\n"
-
- # Добавляем файлы в сообщение
- if found_files:
- reply_text += "Найденные файлы:\n"
- for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
- reply_text += f" Путь: .../{path.split('/')[-2]}/\n"
-
- if len(found_files) > 10:
- reply_text += f"...и еще {len(found_files) - 10} файлов\n"
-
- # Добавляем информацию о общем количестве результатов
- reply_text += f"\nВсего найдено: {total} элементов"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /updates для проверки обновлений"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
- return
-
- try:
- update_info = synology_api.check_for_updates()
-
- if not update_info.get("success", False):
- error = update_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML")
- return
-
- current_version = update_info.get("current_version", "unknown")
- update_available = update_info.get("update_available", False)
- auto_update = update_info.get("auto_update_enabled", False)
- updates = update_info.get("updates", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об обновлениях
- if update_available:
- reply_text = f"🔄 Доступны обновления DSM\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
- reply_text += "Доступные обновления:\n"
-
- for update_item in updates:
- update_name = update_item.get("name", "unknown")
- update_version = update_item.get("version", "unknown")
- update_size = update_item.get("size", 0)
- update_size_str = format_size(update_size)
-
- reply_text += f"• {update_name} v{update_version}\n"
- reply_text += f" └ Размер: {update_size_str}\n"
- else:
- reply_text = f"✅ Система в актуальном состоянии\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /backup для управления резервным копированием"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
- return
-
- try:
- backup_status = synology_api.get_backup_status()
-
- if not backup_status.get("success", False):
- error = backup_status.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML")
- return
-
- backups = backup_status.get("backups", {})
- api_status = backup_status.get("available_apis", {})
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о резервном копировании
- reply_text = f"💾 Резервное копирование Synology NAS\n\n"
-
- # Информация о Hyper Backup
- hyper_backups = backups.get("hyper_backup", [])
- hyper_api_available = api_status.get("hyper_backup", False)
-
- if hyper_api_available:
- reply_text += "Hyper Backup:\n"
-
- if hyper_backups:
- for backup in hyper_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
- last_backup = backup.get("last_backup", "never")
-
- status_emoji = "✅" if status.lower() == "success" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- reply_text += f" └ Последнее копирование: {last_backup}\n"
- else:
- reply_text += "Задачи Hyper Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о Time Backup
- time_backups = backups.get("time_backup", [])
- time_api_available = api_status.get("time_backup", False)
-
- if time_api_available:
- reply_text += "Time Backup:\n"
-
- if time_backups:
- for backup in time_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
-
- status_emoji = "✅" if status.lower() == "normal" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- else:
- reply_text += "Задачи Time Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о USB Copy
- usb_copy = backups.get("usb_copy", {})
- usb_api_available = api_status.get("usb_copy", False)
-
- if usb_api_available:
- usb_enabled = usb_copy.get("enabled", False)
- usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
-
- reply_text += f"USB Copy: {usb_status}\n\n"
-
- # Если ни один из API не доступен
- if not any(api_status.values()):
- reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /reboot для перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед перезагрузкой
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n"
- "Это действие может привести к прерыванию работы всех сервисов.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /sleep для перевода NAS в спящий режим"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед отправкой в спящий режим
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n"
- "Это действие приведет к остановке всех сервисов и отключению NAS.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- # Выполняем перезагрузку
- result = synology_api.reboot_system()
-
- if result:
- # Формируем сообщение об успешной перезагрузке
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /wakeup для включения NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
-
- # Проверяем, не включен ли NAS уже
- if synology_api.is_online(force_check=True):
- await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
- return
-
- try:
- # Отправляем сигнал пробуждения
- result = synology_api.power_on()
-
- if result:
- # Формируем сообщение об успешном включении
- reply_text = "✅ Synology NAS успешно включен\n\n"
- reply_text += "NAS полностью готов к работе."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quota для просмотра информации о квотах"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
- return
-
- try:
- quota_info = synology_api.get_quota_info()
-
- if not quota_info.get("success", False):
- error = quota_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML")
- return
-
- user_quotas = quota_info.get("user_quotas", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о квотах
- reply_text = f"📊 Квоты пользователей Synology NAS\n\n"
-
- if not user_quotas:
- reply_text += "Квоты пользователей не настроены или недоступны"
- else:
- for user_quota in user_quotas:
- user = user_quota.get("user", "unknown")
- quotas = user_quota.get("quotas", [])
-
- if quotas:
- reply_text += f"Пользователь {user}:\n"
-
- for quota in quotas:
- volume = quota.get("volume_name", "unknown")
- limit = quota.get("limit", 0)
- used = quota.get("used", 0)
-
- # Переводим байты в ГБ
- limit_gb = limit / (1024**3) if limit > 0 else 0
- used_gb = used / (1024**3)
-
- # Рассчитываем процент использования
- if limit_gb > 0:
- usage_percent = (used_gb / limit_gb) * 100
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- else:
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
-
- reply_text += "\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления расписанием питания"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("schedule_"):
- action_type = action.split("_")[1]
-
- if action_type == "add_boot":
- # Логика добавления расписания включения
- # В реальном боте здесь будет диалог для настройки расписания
- await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "add_shutdown":
- # Логика добавления расписания выключения
- await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "delete":
- # Логика удаления расписания
- await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
-async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для навигации по файловой системе"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("browse_"):
- path = action[7:] # Убираем префикс "browse_"
-
- # Используем команду browse с указанным путем
- message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках (аналогично функции browse_command)
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action == "confirm_reboot":
- # Выполняем перезагрузку
- message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.reboot_system()
-
- if result:
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_reboot":
- # Отменяем перезагрузку
- await query.edit_message_text("✅ Перезагрузка отменена", parse_mode="HTML")
-
- elif action == "confirm_sleep":
- # Выполняем переход в спящий режим (выключение)
- message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS уже оффлайн\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.power_off()
-
- if result:
- reply_text = "💤 Synology NAS переведен в спящий режим\n\n"
- reply_text += "Для пробуждения используйте команду /wakeup"
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при переходе в спящий режим\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при переходе в спящий режим\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_sleep":
- # Отменяем переход в спящий режим
- await query.edit_message_text("✅ Переход в спящий режим отменен", parse_mode="HTML")
-
-# Вспомогательные функции
-
-def format_size(size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат"""
- if size_bytes < 1024:
- return f"{size_bytes} Б"
- elif size_bytes < 1024**2:
- return f"{size_bytes/1024:.1f} КБ"
- elif size_bytes < 1024**3:
- return f"{size_bytes/1024**2:.1f} МБ"
- else:
- return f"{size_bytes/1024**3:.1f} ГБ"
-
-def get_file_icon(filename: str) -> str:
- """Возвращает эмодзи-иконку в зависимости от типа файла"""
- extension = filename.lower().split('.')[-1] if '.' in filename else ''
-
- if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
- return "🖼️"
- elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
- return "🎬"
- elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
- return "🎵"
- elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
- return "📄"
- elif extension in ['xls', 'xlsx', 'csv']:
- return "📊"
- elif extension in ['ppt', 'pptx']:
- return "📑"
- elif extension in ['pdf']:
- return "📕"
- elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
- return "🗜️"
- elif extension in ['exe', 'msi']:
- return "⚙️"
- else:
- return "📄"
diff --git a/.history/src/handlers/advanced_handlers_20250830093627.py b/.history/src/handlers/advanced_handlers_20250830093627.py
deleted file mode 100644
index 9cc9ee6..0000000
--- a/.history/src/handlers/advanced_handlers_20250830093627.py
+++ /dev/null
@@ -1,972 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Расширенные обработчики команд для управления Synology NAS
-"""
-
-import logging
-from datetime import datetime
-from typing import List, Dict, Any
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /processes для получения списка активных процессов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
- return
-
- try:
- processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
-
- if not processes:
- await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о процессах
- reply_text = f"⚙️ Активные процессы Synology NAS\n\n"
-
- for process in processes:
- name = process.get("name", "unknown")
- pid = process.get("pid", "?")
- cpu_usage = process.get("cpu_usage", 0)
- memory_usage = process.get("memory_usage", 0)
-
- reply_text += f"• {name} (PID: {pid})\n"
- reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
-
- reply_text += f"\nПоказано {len(processes)} наиболее активных процессов"
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /network для получения информации о сетевых подключениях"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
- return
-
- try:
- network_status = synology_api.get_network_status()
-
- if not network_status:
- await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о сетевых интерфейсах
- interfaces = network_status.get("interfaces", [])
-
- reply_text = f"🌐 Сетевые подключения Synology NAS\n\n"
-
- for interface in interfaces:
- name = interface.get("id", "unknown")
- ip = interface.get("ip", "Нет данных")
- mac = interface.get("mac", "Нет данных")
- status = "Активен" if interface.get("status") else "Неактивен"
-
- # Информация о трафике
- rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
- tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- if rx_bytes > 0 or tx_bytes > 0:
- reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /temperature для мониторинга температуры"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о температуре...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
- return
-
- try:
- temp_status = synology_api.get_temperature_status()
-
- if not temp_status:
- await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о температуре
- system_temp = temp_status.get("system_temperature")
- disk_temps = temp_status.get("disk_temperatures", [])
- is_warning = temp_status.get("warning", False)
-
- # Выбор emoji в зависимости от температуры
- temp_emoji = "🔥" if is_warning else "🌡️"
-
- reply_text = f"{temp_emoji} Температура Synology NAS\n\n"
-
- if system_temp is not None:
- temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная"
- reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n"
-
- if disk_temps:
- reply_text += "Температура дисков:\n"
- for disk in disk_temps:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temperature", 0)
-
- disk_temp_emoji = "🔥" if temp > 45 else "✅"
- reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /schedule для управления расписанием питания"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
- return
-
- try:
- schedule = synology_api.get_power_schedule()
-
- if not schedule:
- await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о расписании питания
- boot_tasks = schedule.get("boot_tasks", [])
- shutdown_tasks = schedule.get("shutdown_tasks", [])
-
- reply_text = f"⏱️ Расписание питания Synology NAS\n\n"
-
- if boot_tasks:
- reply_text += "Расписание включения:\n"
- for task in boot_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание включения: Не настроено\n"
-
- reply_text += "\n"
-
- if shutdown_tasks:
- reply_text += "Расписание выключения:\n"
- for task in shutdown_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание выключения: Не настроено\n"
-
- # Добавляем кнопки для управления расписанием
- keyboard = [
- [
- InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"),
- InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown")
- ],
- [
- InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
-
-async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /browse для просмотра файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем путь из аргументов команды или используем корневую директорию
- path = " ".join(context.args) if context.args else ""
-
- message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /search для поиска файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем шаблон поиска из аргументов команды
- if not context.args:
- await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>")
- return
-
- pattern = " ".join(context.args)
-
- message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
- return
-
- try:
- search_result = synology_api.search_files(pattern=pattern, limit=20)
-
- if not search_result.get("success", False):
- error = search_result.get("error", "unknown")
- progress = search_result.get("progress", 0)
-
- if error == "search_timeout":
- await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
- else:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- files = search_result.get("results", [])
- total = search_result.get("total", len(files))
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение с результатами поиска
- reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n"
-
- if not files:
- reply_text += "📭 Файлы не найдены"
- else:
- # Сортируем: сначала папки, потом файлы
- folders = []
- found_files = []
-
- for item in files:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path))
- else:
- # Для файлов получаем размер и путь к родительской папке
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- parent_path = "/".join(path.split("/")[:-1])
- found_files.append((name, path, size_str, parent_path))
-
- # Добавляем папки в сообщение
- if folders:
- reply_text += "Найденные папки:\n"
- for name, path in folders[:5]: # Показываем первые 5 папок
- reply_text += f"📁 {name}\n"
-
- if len(folders) > 5:
- reply_text += f"...и еще {len(folders) - 5} папок\n"
-
- reply_text += "\n"
-
- # Добавляем файлы в сообщение
- if found_files:
- reply_text += "Найденные файлы:\n"
- for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
- reply_text += f" Путь: .../{path.split('/')[-2]}/\n"
-
- if len(found_files) > 10:
- reply_text += f"...и еще {len(found_files) - 10} файлов\n"
-
- # Добавляем информацию о общем количестве результатов
- reply_text += f"\nВсего найдено: {total} элементов"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /updates для проверки обновлений"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
- return
-
- try:
- update_info = synology_api.check_for_updates()
-
- if not update_info.get("success", False):
- error = update_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML")
- return
-
- current_version = update_info.get("current_version", "unknown")
- update_available = update_info.get("update_available", False)
- auto_update = update_info.get("auto_update_enabled", False)
- updates = update_info.get("updates", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об обновлениях
- if update_available:
- reply_text = f"🔄 Доступны обновления DSM\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
- reply_text += "Доступные обновления:\n"
-
- for update_item in updates:
- update_name = update_item.get("name", "unknown")
- update_version = update_item.get("version", "unknown")
- update_size = update_item.get("size", 0)
- update_size_str = format_size(update_size)
-
- reply_text += f"• {update_name} v{update_version}\n"
- reply_text += f" └ Размер: {update_size_str}\n"
- else:
- reply_text = f"✅ Система в актуальном состоянии\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /backup для управления резервным копированием"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
- return
-
- try:
- backup_status = synology_api.get_backup_status()
-
- if not backup_status.get("success", False):
- error = backup_status.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML")
- return
-
- backups = backup_status.get("backups", {})
- api_status = backup_status.get("available_apis", {})
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о резервном копировании
- reply_text = f"💾 Резервное копирование Synology NAS\n\n"
-
- # Информация о Hyper Backup
- hyper_backups = backups.get("hyper_backup", [])
- hyper_api_available = api_status.get("hyper_backup", False)
-
- if hyper_api_available:
- reply_text += "Hyper Backup:\n"
-
- if hyper_backups:
- for backup in hyper_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
- last_backup = backup.get("last_backup", "never")
-
- status_emoji = "✅" if status.lower() == "success" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- reply_text += f" └ Последнее копирование: {last_backup}\n"
- else:
- reply_text += "Задачи Hyper Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о Time Backup
- time_backups = backups.get("time_backup", [])
- time_api_available = api_status.get("time_backup", False)
-
- if time_api_available:
- reply_text += "Time Backup:\n"
-
- if time_backups:
- for backup in time_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
-
- status_emoji = "✅" if status.lower() == "normal" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- else:
- reply_text += "Задачи Time Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о USB Copy
- usb_copy = backups.get("usb_copy", {})
- usb_api_available = api_status.get("usb_copy", False)
-
- if usb_api_available:
- usb_enabled = usb_copy.get("enabled", False)
- usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
-
- reply_text += f"USB Copy: {usb_status}\n\n"
-
- # Если ни один из API не доступен
- if not any(api_status.values()):
- reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /reboot для перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед перезагрузкой
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n"
- "Это действие может привести к прерыванию работы всех сервисов.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /sleep для перевода NAS в спящий режим"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед отправкой в спящий режим
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n"
- "Это действие приведет к остановке всех сервисов и отключению NAS.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- # Выполняем перезагрузку
- result = synology_api.reboot_system()
-
- if result:
- # Формируем сообщение об успешной перезагрузке
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /wakeup для включения NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
-
- # Проверяем, не включен ли NAS уже
- if synology_api.is_online(force_check=True):
- await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
- return
-
- try:
- # Отправляем сигнал пробуждения
- result = synology_api.power_on()
-
- if result:
- # Формируем сообщение об успешном включении
- reply_text = "✅ Synology NAS успешно включен\n\n"
- reply_text += "NAS полностью готов к работе."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quota для просмотра информации о квотах"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
- return
-
- try:
- quota_info = synology_api.get_quota_info()
-
- if not quota_info.get("success", False):
- error = quota_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML")
- return
-
- user_quotas = quota_info.get("user_quotas", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о квотах
- reply_text = f"📊 Квоты пользователей Synology NAS\n\n"
-
- if not user_quotas:
- reply_text += "Квоты пользователей не настроены или недоступны"
- else:
- for user_quota in user_quotas:
- user = user_quota.get("user", "unknown")
- quotas = user_quota.get("quotas", [])
-
- if quotas:
- reply_text += f"Пользователь {user}:\n"
-
- for quota in quotas:
- volume = quota.get("volume_name", "unknown")
- limit = quota.get("limit", 0)
- used = quota.get("used", 0)
-
- # Переводим байты в ГБ
- limit_gb = limit / (1024**3) if limit > 0 else 0
- used_gb = used / (1024**3)
-
- # Рассчитываем процент использования
- if limit_gb > 0:
- usage_percent = (used_gb / limit_gb) * 100
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- else:
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
-
- reply_text += "\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления расписанием питания"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("schedule_"):
- action_type = action.split("_")[1]
-
- if action_type == "add_boot":
- # Логика добавления расписания включения
- # В реальном боте здесь будет диалог для настройки расписания
- await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "add_shutdown":
- # Логика добавления расписания выключения
- await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "delete":
- # Логика удаления расписания
- await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
-async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для навигации по файловой системе"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("browse_"):
- path = action[7:] # Убираем префикс "browse_"
-
- # Используем команду browse с указанным путем
- message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках (аналогично функции browse_command)
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action == "confirm_reboot":
- # Выполняем перезагрузку
- message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.reboot_system()
-
- if result:
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_reboot":
- # Отменяем перезагрузку
- await query.edit_message_text("✅ Перезагрузка отменена", parse_mode="HTML")
-
- elif action == "confirm_sleep":
- # Выполняем переход в спящий режим (выключение)
- message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS уже оффлайн\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.power_off()
-
- if result:
- reply_text = "💤 Synology NAS переведен в спящий режим\n\n"
- reply_text += "Для пробуждения используйте команду /wakeup"
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при переходе в спящий режим\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при переходе в спящий режим\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_sleep":
- # Отменяем переход в спящий режим
- await query.edit_message_text("✅ Переход в спящий режим отменен", parse_mode="HTML")
-
-# Вспомогательные функции
-
-def format_size(size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат"""
- if size_bytes < 1024:
- return f"{size_bytes} Б"
- elif size_bytes < 1024**2:
- return f"{size_bytes/1024:.1f} КБ"
- elif size_bytes < 1024**3:
- return f"{size_bytes/1024**2:.1f} МБ"
- else:
- return f"{size_bytes/1024**3:.1f} ГБ"
-
-def get_file_icon(filename: str) -> str:
- """Возвращает эмодзи-иконку в зависимости от типа файла"""
- extension = filename.lower().split('.')[-1] if '.' in filename else ''
-
- if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
- return "🖼️"
- elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
- return "🎬"
- elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
- return "🎵"
- elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
- return "📄"
- elif extension in ['xls', 'xlsx', 'csv']:
- return "📊"
- elif extension in ['ppt', 'pptx']:
- return "📑"
- elif extension in ['pdf']:
- return "📕"
- elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
- return "🗜️"
- elif extension in ['exe', 'msi']:
- return "⚙️"
- else:
- return "📄"
diff --git a/.history/src/handlers/advanced_handlers_20250830094738.py b/.history/src/handlers/advanced_handlers_20250830094738.py
deleted file mode 100644
index 9cc9ee6..0000000
--- a/.history/src/handlers/advanced_handlers_20250830094738.py
+++ /dev/null
@@ -1,972 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Расширенные обработчики команд для управления Synology NAS
-"""
-
-import logging
-from datetime import datetime
-from typing import List, Dict, Any
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /processes для получения списка активных процессов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
- return
-
- try:
- processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
-
- if not processes:
- await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о процессах
- reply_text = f"⚙️ Активные процессы Synology NAS\n\n"
-
- for process in processes:
- name = process.get("name", "unknown")
- pid = process.get("pid", "?")
- cpu_usage = process.get("cpu_usage", 0)
- memory_usage = process.get("memory_usage", 0)
-
- reply_text += f"• {name} (PID: {pid})\n"
- reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
-
- reply_text += f"\nПоказано {len(processes)} наиболее активных процессов"
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /network для получения информации о сетевых подключениях"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
- return
-
- try:
- network_status = synology_api.get_network_status()
-
- if not network_status:
- await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о сетевых интерфейсах
- interfaces = network_status.get("interfaces", [])
-
- reply_text = f"🌐 Сетевые подключения Synology NAS\n\n"
-
- for interface in interfaces:
- name = interface.get("id", "unknown")
- ip = interface.get("ip", "Нет данных")
- mac = interface.get("mac", "Нет данных")
- status = "Активен" if interface.get("status") else "Неактивен"
-
- # Информация о трафике
- rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
- tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- if rx_bytes > 0 or tx_bytes > 0:
- reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /temperature для мониторинга температуры"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о температуре...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
- return
-
- try:
- temp_status = synology_api.get_temperature_status()
-
- if not temp_status:
- await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о температуре
- system_temp = temp_status.get("system_temperature")
- disk_temps = temp_status.get("disk_temperatures", [])
- is_warning = temp_status.get("warning", False)
-
- # Выбор emoji в зависимости от температуры
- temp_emoji = "🔥" if is_warning else "🌡️"
-
- reply_text = f"{temp_emoji} Температура Synology NAS\n\n"
-
- if system_temp is not None:
- temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная"
- reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n"
-
- if disk_temps:
- reply_text += "Температура дисков:\n"
- for disk in disk_temps:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temperature", 0)
-
- disk_temp_emoji = "🔥" if temp > 45 else "✅"
- reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /schedule для управления расписанием питания"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
- return
-
- try:
- schedule = synology_api.get_power_schedule()
-
- if not schedule:
- await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о расписании питания
- boot_tasks = schedule.get("boot_tasks", [])
- shutdown_tasks = schedule.get("shutdown_tasks", [])
-
- reply_text = f"⏱️ Расписание питания Synology NAS\n\n"
-
- if boot_tasks:
- reply_text += "Расписание включения:\n"
- for task in boot_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание включения: Не настроено\n"
-
- reply_text += "\n"
-
- if shutdown_tasks:
- reply_text += "Расписание выключения:\n"
- for task in shutdown_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание выключения: Не настроено\n"
-
- # Добавляем кнопки для управления расписанием
- keyboard = [
- [
- InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"),
- InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown")
- ],
- [
- InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
-
-async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /browse для просмотра файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем путь из аргументов команды или используем корневую директорию
- path = " ".join(context.args) if context.args else ""
-
- message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /search для поиска файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем шаблон поиска из аргументов команды
- if not context.args:
- await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>")
- return
-
- pattern = " ".join(context.args)
-
- message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
- return
-
- try:
- search_result = synology_api.search_files(pattern=pattern, limit=20)
-
- if not search_result.get("success", False):
- error = search_result.get("error", "unknown")
- progress = search_result.get("progress", 0)
-
- if error == "search_timeout":
- await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
- else:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- files = search_result.get("results", [])
- total = search_result.get("total", len(files))
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение с результатами поиска
- reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n"
-
- if not files:
- reply_text += "📭 Файлы не найдены"
- else:
- # Сортируем: сначала папки, потом файлы
- folders = []
- found_files = []
-
- for item in files:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path))
- else:
- # Для файлов получаем размер и путь к родительской папке
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- parent_path = "/".join(path.split("/")[:-1])
- found_files.append((name, path, size_str, parent_path))
-
- # Добавляем папки в сообщение
- if folders:
- reply_text += "Найденные папки:\n"
- for name, path in folders[:5]: # Показываем первые 5 папок
- reply_text += f"📁 {name}\n"
-
- if len(folders) > 5:
- reply_text += f"...и еще {len(folders) - 5} папок\n"
-
- reply_text += "\n"
-
- # Добавляем файлы в сообщение
- if found_files:
- reply_text += "Найденные файлы:\n"
- for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
- reply_text += f" Путь: .../{path.split('/')[-2]}/\n"
-
- if len(found_files) > 10:
- reply_text += f"...и еще {len(found_files) - 10} файлов\n"
-
- # Добавляем информацию о общем количестве результатов
- reply_text += f"\nВсего найдено: {total} элементов"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /updates для проверки обновлений"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
- return
-
- try:
- update_info = synology_api.check_for_updates()
-
- if not update_info.get("success", False):
- error = update_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML")
- return
-
- current_version = update_info.get("current_version", "unknown")
- update_available = update_info.get("update_available", False)
- auto_update = update_info.get("auto_update_enabled", False)
- updates = update_info.get("updates", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об обновлениях
- if update_available:
- reply_text = f"🔄 Доступны обновления DSM\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
- reply_text += "Доступные обновления:\n"
-
- for update_item in updates:
- update_name = update_item.get("name", "unknown")
- update_version = update_item.get("version", "unknown")
- update_size = update_item.get("size", 0)
- update_size_str = format_size(update_size)
-
- reply_text += f"• {update_name} v{update_version}\n"
- reply_text += f" └ Размер: {update_size_str}\n"
- else:
- reply_text = f"✅ Система в актуальном состоянии\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /backup для управления резервным копированием"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
- return
-
- try:
- backup_status = synology_api.get_backup_status()
-
- if not backup_status.get("success", False):
- error = backup_status.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML")
- return
-
- backups = backup_status.get("backups", {})
- api_status = backup_status.get("available_apis", {})
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о резервном копировании
- reply_text = f"💾 Резервное копирование Synology NAS\n\n"
-
- # Информация о Hyper Backup
- hyper_backups = backups.get("hyper_backup", [])
- hyper_api_available = api_status.get("hyper_backup", False)
-
- if hyper_api_available:
- reply_text += "Hyper Backup:\n"
-
- if hyper_backups:
- for backup in hyper_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
- last_backup = backup.get("last_backup", "never")
-
- status_emoji = "✅" if status.lower() == "success" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- reply_text += f" └ Последнее копирование: {last_backup}\n"
- else:
- reply_text += "Задачи Hyper Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о Time Backup
- time_backups = backups.get("time_backup", [])
- time_api_available = api_status.get("time_backup", False)
-
- if time_api_available:
- reply_text += "Time Backup:\n"
-
- if time_backups:
- for backup in time_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
-
- status_emoji = "✅" if status.lower() == "normal" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- else:
- reply_text += "Задачи Time Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о USB Copy
- usb_copy = backups.get("usb_copy", {})
- usb_api_available = api_status.get("usb_copy", False)
-
- if usb_api_available:
- usb_enabled = usb_copy.get("enabled", False)
- usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
-
- reply_text += f"USB Copy: {usb_status}\n\n"
-
- # Если ни один из API не доступен
- if not any(api_status.values()):
- reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /reboot для перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед перезагрузкой
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n"
- "Это действие может привести к прерыванию работы всех сервисов.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /sleep для перевода NAS в спящий режим"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед отправкой в спящий режим
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n"
- "Это действие приведет к остановке всех сервисов и отключению NAS.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- # Выполняем перезагрузку
- result = synology_api.reboot_system()
-
- if result:
- # Формируем сообщение об успешной перезагрузке
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /wakeup для включения NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
-
- # Проверяем, не включен ли NAS уже
- if synology_api.is_online(force_check=True):
- await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
- return
-
- try:
- # Отправляем сигнал пробуждения
- result = synology_api.power_on()
-
- if result:
- # Формируем сообщение об успешном включении
- reply_text = "✅ Synology NAS успешно включен\n\n"
- reply_text += "NAS полностью готов к работе."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quota для просмотра информации о квотах"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
- return
-
- try:
- quota_info = synology_api.get_quota_info()
-
- if not quota_info.get("success", False):
- error = quota_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML")
- return
-
- user_quotas = quota_info.get("user_quotas", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о квотах
- reply_text = f"📊 Квоты пользователей Synology NAS\n\n"
-
- if not user_quotas:
- reply_text += "Квоты пользователей не настроены или недоступны"
- else:
- for user_quota in user_quotas:
- user = user_quota.get("user", "unknown")
- quotas = user_quota.get("quotas", [])
-
- if quotas:
- reply_text += f"Пользователь {user}:\n"
-
- for quota in quotas:
- volume = quota.get("volume_name", "unknown")
- limit = quota.get("limit", 0)
- used = quota.get("used", 0)
-
- # Переводим байты в ГБ
- limit_gb = limit / (1024**3) if limit > 0 else 0
- used_gb = used / (1024**3)
-
- # Рассчитываем процент использования
- if limit_gb > 0:
- usage_percent = (used_gb / limit_gb) * 100
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- else:
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
-
- reply_text += "\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления расписанием питания"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("schedule_"):
- action_type = action.split("_")[1]
-
- if action_type == "add_boot":
- # Логика добавления расписания включения
- # В реальном боте здесь будет диалог для настройки расписания
- await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "add_shutdown":
- # Логика добавления расписания выключения
- await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "delete":
- # Логика удаления расписания
- await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
-async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для навигации по файловой системе"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("browse_"):
- path = action[7:] # Убираем префикс "browse_"
-
- # Используем команду browse с указанным путем
- message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках (аналогично функции browse_command)
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action == "confirm_reboot":
- # Выполняем перезагрузку
- message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.reboot_system()
-
- if result:
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_reboot":
- # Отменяем перезагрузку
- await query.edit_message_text("✅ Перезагрузка отменена", parse_mode="HTML")
-
- elif action == "confirm_sleep":
- # Выполняем переход в спящий режим (выключение)
- message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS уже оффлайн\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.power_off()
-
- if result:
- reply_text = "💤 Synology NAS переведен в спящий режим\n\n"
- reply_text += "Для пробуждения используйте команду /wakeup"
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при переходе в спящий режим\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при переходе в спящий режим\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_sleep":
- # Отменяем переход в спящий режим
- await query.edit_message_text("✅ Переход в спящий режим отменен", parse_mode="HTML")
-
-# Вспомогательные функции
-
-def format_size(size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат"""
- if size_bytes < 1024:
- return f"{size_bytes} Б"
- elif size_bytes < 1024**2:
- return f"{size_bytes/1024:.1f} КБ"
- elif size_bytes < 1024**3:
- return f"{size_bytes/1024**2:.1f} МБ"
- else:
- return f"{size_bytes/1024**3:.1f} ГБ"
-
-def get_file_icon(filename: str) -> str:
- """Возвращает эмодзи-иконку в зависимости от типа файла"""
- extension = filename.lower().split('.')[-1] if '.' in filename else ''
-
- if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
- return "🖼️"
- elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
- return "🎬"
- elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
- return "🎵"
- elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
- return "📄"
- elif extension in ['xls', 'xlsx', 'csv']:
- return "📊"
- elif extension in ['ppt', 'pptx']:
- return "📑"
- elif extension in ['pdf']:
- return "📕"
- elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
- return "🗜️"
- elif extension in ['exe', 'msi']:
- return "⚙️"
- else:
- return "📄"
diff --git a/.history/src/handlers/advanced_handlers_20250830104205.py b/.history/src/handlers/advanced_handlers_20250830104205.py
deleted file mode 100644
index 315405b..0000000
--- a/.history/src/handlers/advanced_handlers_20250830104205.py
+++ /dev/null
@@ -1,972 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Расширенные обработчики команд для управления Synology NAS
-"""
-
-import logging
-from datetime import datetime
-from typing import List, Dict, Any
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /processes для получения списка активных процессов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
- return
-
- try:
- processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
-
- if not processes:
- await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о процессах
- reply_text = f"⚙️ Активные процессы Synology NAS\n\n"
-
- for process in processes:
- name = process.get("name", "unknown")
- pid = process.get("pid", "?")
- cpu_usage = process.get("cpu_usage", 0)
- memory_usage = process.get("memory_usage", 0)
-
- reply_text += f"• {name} (PID: {pid})\n"
- reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
-
- reply_text += f"\nПоказано {len(processes)} наиболее активных процессов"
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /network для получения информации о сетевых подключениях"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
- return
-
- try:
- network_status = synology_api.get_network_status()
-
- if not network_status:
- await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о сетевых интерфейсах
- interfaces = network_status.get("interfaces", [])
-
- reply_text = f"🌐 Сетевые подключения Synology NAS\n\n"
-
- for interface in interfaces:
- name = interface.get("id", "unknown")
- ip = interface.get("ip", "Нет данных")
- mac = interface.get("mac", "Нет данных")
- status = "Активен" if interface.get("status") else "Неактивен"
-
- # Информация о трафике
- rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
- tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- if rx_bytes > 0 or tx_bytes > 0:
- reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /temperature для мониторинга температуры"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о температуре...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
- return
-
- try:
- temp_status = synology_api.get_temperature_status()
-
- if not temp_status:
- await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о температуре
- system_temp = temp_status.get("system_temperature")
- disk_temps = temp_status.get("disk_temperatures", [])
- is_warning = temp_status.get("warning", False)
-
- # Выбор emoji в зависимости от температуры
- temp_emoji = "🔥" if is_warning else "🌡️"
-
- reply_text = f"{temp_emoji} Температура Synology NAS\n\n"
-
- if system_temp is not None:
- temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная"
- reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n"
-
- if disk_temps:
- reply_text += "Температура дисков:\n"
- for disk in disk_temps:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temperature", 0)
-
- disk_temp_emoji = "🔥" if temp > 45 else "✅"
- reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /schedule для управления расписанием питания"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
- return
-
- try:
- schedule = synology_api.get_power_schedule()
-
- if not schedule:
- await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о расписании питания
- boot_tasks = schedule.get("boot_tasks", [])
- shutdown_tasks = schedule.get("shutdown_tasks", [])
-
- reply_text = f"⏱️ Расписание питания Synology NAS\n\n"
-
- if boot_tasks:
- reply_text += "Расписание включения:\n"
- for task in boot_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание включения: Не настроено\n"
-
- reply_text += "\n"
-
- if shutdown_tasks:
- reply_text += "Расписание выключения:\n"
- for task in shutdown_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание выключения: Не настроено\n"
-
- # Добавляем кнопки для управления расписанием
- keyboard = [
- [
- InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"),
- InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown")
- ],
- [
- InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
-
-async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /browse для просмотра файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем путь из аргументов команды или используем корневую директорию
- path = " ".join(context.args) if context.args else ""
-
- message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /search для поиска файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем шаблон поиска из аргументов команды
- if not context.args:
- await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>")
- return
-
- pattern = " ".join(context.args)
-
- message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
- return
-
- try:
- search_result = synology_api.search_files(pattern=pattern, limit=20)
-
- if not search_result.get("success", False):
- error = search_result.get("error", "unknown")
- progress = search_result.get("progress", 0)
-
- if error == "search_timeout":
- await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
- else:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- files = search_result.get("results", [])
- total = search_result.get("total", len(files))
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение с результатами поиска
- reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n"
-
- if not files:
- reply_text += "📭 Файлы не найдены"
- else:
- # Сортируем: сначала папки, потом файлы
- folders = []
- found_files = []
-
- for item in files:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path))
- else:
- # Для файлов получаем размер и путь к родительской папке
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- parent_path = "/".join(path.split("/")[:-1])
- found_files.append((name, path, size_str, parent_path))
-
- # Добавляем папки в сообщение
- if folders:
- reply_text += "Найденные папки:\n"
- for name, path in folders[:5]: # Показываем первые 5 папок
- reply_text += f"📁 {name}\n"
-
- if len(folders) > 5:
- reply_text += f"...и еще {len(folders) - 5} папок\n"
-
- reply_text += "\n"
-
- # Добавляем файлы в сообщение
- if found_files:
- reply_text += "Найденные файлы:\n"
- for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
- reply_text += f" Путь: .../{path.split('/')[-2]}/\n"
-
- if len(found_files) > 10:
- reply_text += f"...и еще {len(found_files) - 10} файлов\n"
-
- # Добавляем информацию о общем количестве результатов
- reply_text += f"\nВсего найдено: {total} элементов"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /updates для проверки обновлений"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
- return
-
- try:
- update_info = synology_api.check_for_updates()
-
- if not update_info.get("success", False):
- error = update_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML")
- return
-
- current_version = update_info.get("current_version", "unknown")
- update_available = update_info.get("update_available", False)
- auto_update = update_info.get("auto_update_enabled", False)
- updates = update_info.get("updates", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об обновлениях
- if update_available:
- reply_text = f"🔄 Доступны обновления DSM\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
- reply_text += "Доступные обновления:\n"
-
- for update_item in updates:
- update_name = update_item.get("name", "unknown")
- update_version = update_item.get("version", "unknown")
- update_size = update_item.get("size", 0)
- update_size_str = format_size(update_size)
-
- reply_text += f"• {update_name} v{update_version}\n"
- reply_text += f" └ Размер: {update_size_str}\n"
- else:
- reply_text = f"✅ Система в актуальном состоянии\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /backup для управления резервным копированием"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
- return
-
- try:
- backup_status = synology_api.get_backup_status()
-
- if not backup_status.get("success", False):
- error = backup_status.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML")
- return
-
- backups = backup_status.get("backups", {})
- api_status = backup_status.get("available_apis", {})
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о резервном копировании
- reply_text = f"💾 Резервное копирование Synology NAS\n\n"
-
- # Информация о Hyper Backup
- hyper_backups = backups.get("hyper_backup", [])
- hyper_api_available = api_status.get("hyper_backup", False)
-
- if hyper_api_available:
- reply_text += "Hyper Backup:\n"
-
- if hyper_backups:
- for backup in hyper_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
- last_backup = backup.get("last_backup", "never")
-
- status_emoji = "✅" if status.lower() == "success" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- reply_text += f" └ Последнее копирование: {last_backup}\n"
- else:
- reply_text += "Задачи Hyper Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о Time Backup
- time_backups = backups.get("time_backup", [])
- time_api_available = api_status.get("time_backup", False)
-
- if time_api_available:
- reply_text += "Time Backup:\n"
-
- if time_backups:
- for backup in time_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
-
- status_emoji = "✅" if status.lower() == "normal" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- else:
- reply_text += "Задачи Time Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о USB Copy
- usb_copy = backups.get("usb_copy", {})
- usb_api_available = api_status.get("usb_copy", False)
-
- if usb_api_available:
- usb_enabled = usb_copy.get("enabled", False)
- usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
-
- reply_text += f"USB Copy: {usb_status}\n\n"
-
- # Если ни один из API не доступен
- if not any(api_status.values()):
- reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /reboot для перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед перезагрузкой
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n"
- "Это действие может привести к прерыванию работы всех сервисов.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /sleep для перевода NAS в спящий режим"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед отправкой в спящий режим
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n"
- "Это действие приведет к остановке всех сервисов и отключению NAS.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- # Выполняем перезагрузку
- result = synology_api.reboot_system()
-
- if result:
- # Формируем сообщение об успешной перезагрузке
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /wakeup для включения NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
-
- # Проверяем, не включен ли NAS уже
- if synology_api.is_online(force_check=True):
- await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
- return
-
- try:
- # Отправляем сигнал пробуждения
- result = synology_api.power_on()
-
- if result:
- # Формируем сообщение об успешном включении
- reply_text = "✅ Synology NAS успешно включен\n\n"
- reply_text += "NAS полностью готов к работе."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quota для просмотра информации о квотах"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
- return
-
- try:
- quota_info = synology_api.get_quota_info()
-
- if not quota_info.get("success", False):
- error = quota_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML")
- return
-
- user_quotas = quota_info.get("user_quotas", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о квотах
- reply_text = f"📊 Квоты пользователей Synology NAS\n\n"
-
- if not user_quotas:
- reply_text += "Квоты пользователей не настроены или недоступны"
- else:
- for user_quota in user_quotas:
- user = user_quota.get("user", "unknown")
- quotas = user_quota.get("quotas", [])
-
- if quotas:
- reply_text += f"Пользователь {user}:\n"
-
- for quota in quotas:
- volume = quota.get("volume_name", "unknown")
- limit = quota.get("limit", 0)
- used = quota.get("used", 0)
-
- # Переводим байты в ГБ
- limit_gb = limit / (1024**3) if limit > 0 else 0
- used_gb = used / (1024**3)
-
- # Рассчитываем процент использования
- if limit_gb > 0:
- usage_percent = (used_gb / limit_gb) * 100
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- else:
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
-
- reply_text += "\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления расписанием питания"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("schedule_"):
- action_type = action.split("_")[1]
-
- if action_type == "add_boot":
- # Логика добавления расписания включения
- # В реальном боте здесь будет диалог для настройки расписания
- await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "add_shutdown":
- # Логика добавления расписания выключения
- await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "delete":
- # Логика удаления расписания
- await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
-async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для навигации по файловой системе"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("browse_"):
- path = action[7:] # Убираем префикс "browse_"
-
- # Используем команду browse с указанным путем
- message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках (аналогично функции browse_command)
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action == "confirm_reboot":
- # Выполняем перезагрузку
- message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.reboot_system()
-
- if result:
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_reboot":
- # Отменяем перезагрузку
- await query.edit_message_text("✅ Перезагрузка отменена", parse_mode="HTML")
-
- elif action == "confirm_sleep":
- # Выполняем переход в спящий режим (выключение)
- message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS уже оффлайн\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.power_off()
-
- if result:
- reply_text = "💤 Synology NAS переведен в спящий режим\n\n"
- reply_text += "Для пробуждения используйте команду /wakeup"
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при переходе в спящий режим\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при переходе в спящий режим\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_sleep":
- # Отменяем переход в спящий режим
- await query.edit_message_text("✅ Переход в спящий режим отменен", parse_mode="HTML")
-
-# Вспомогательные функции
-
-def format_size(size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат"""
- if size_bytes < 1024:
- return f"{size_bytes} Б"
- elif size_bytes < 1024**2:
- return f"{size_bytes/1024:.1f} КБ"
- elif size_bytes < 1024**3:
- return f"{size_bytes/1024**2:.1f} МБ"
- else:
- return f"{size_bytes/1024**3:.1f} ГБ"
-
-def get_file_icon(filename: str) -> str:
- """Возвращает эмодзи-иконку в зависимости от типа файла"""
- extension = filename.lower().split('.')[-1] if '.' in filename else ''
-
- if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
- return "🖼️"
- elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
- return "🎬"
- elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
- return "🎵"
- elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
- return "📄"
- elif extension in ['xls', 'xlsx', 'csv']:
- return "📊"
- elif extension in ['ppt', 'pptx']:
- return "📑"
- elif extension in ['pdf']:
- return "📕"
- elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
- return "🗜️"
- elif extension in ['exe', 'msi']:
- return "⚙️"
- else:
- return "📄"
diff --git a/.history/src/handlers/advanced_handlers_20250830104340.py b/.history/src/handlers/advanced_handlers_20250830104340.py
deleted file mode 100644
index 315405b..0000000
--- a/.history/src/handlers/advanced_handlers_20250830104340.py
+++ /dev/null
@@ -1,972 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Расширенные обработчики команд для управления Synology NAS
-"""
-
-import logging
-from datetime import datetime
-from typing import List, Dict, Any
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /processes для получения списка активных процессов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
- return
-
- try:
- processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
-
- if not processes:
- await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о процессах
- reply_text = f"⚙️ Активные процессы Synology NAS\n\n"
-
- for process in processes:
- name = process.get("name", "unknown")
- pid = process.get("pid", "?")
- cpu_usage = process.get("cpu_usage", 0)
- memory_usage = process.get("memory_usage", 0)
-
- reply_text += f"• {name} (PID: {pid})\n"
- reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
-
- reply_text += f"\nПоказано {len(processes)} наиболее активных процессов"
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /network для получения информации о сетевых подключениях"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
- return
-
- try:
- network_status = synology_api.get_network_status()
-
- if not network_status:
- await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о сетевых интерфейсах
- interfaces = network_status.get("interfaces", [])
-
- reply_text = f"🌐 Сетевые подключения Synology NAS\n\n"
-
- for interface in interfaces:
- name = interface.get("id", "unknown")
- ip = interface.get("ip", "Нет данных")
- mac = interface.get("mac", "Нет данных")
- status = "Активен" if interface.get("status") else "Неактивен"
-
- # Информация о трафике
- rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
- tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- if rx_bytes > 0 or tx_bytes > 0:
- reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /temperature для мониторинга температуры"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о температуре...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
- return
-
- try:
- temp_status = synology_api.get_temperature_status()
-
- if not temp_status:
- await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о температуре
- system_temp = temp_status.get("system_temperature")
- disk_temps = temp_status.get("disk_temperatures", [])
- is_warning = temp_status.get("warning", False)
-
- # Выбор emoji в зависимости от температуры
- temp_emoji = "🔥" if is_warning else "🌡️"
-
- reply_text = f"{temp_emoji} Температура Synology NAS\n\n"
-
- if system_temp is not None:
- temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная"
- reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n"
-
- if disk_temps:
- reply_text += "Температура дисков:\n"
- for disk in disk_temps:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temperature", 0)
-
- disk_temp_emoji = "🔥" if temp > 45 else "✅"
- reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /schedule для управления расписанием питания"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
- return
-
- try:
- schedule = synology_api.get_power_schedule()
-
- if not schedule:
- await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о расписании питания
- boot_tasks = schedule.get("boot_tasks", [])
- shutdown_tasks = schedule.get("shutdown_tasks", [])
-
- reply_text = f"⏱️ Расписание питания Synology NAS\n\n"
-
- if boot_tasks:
- reply_text += "Расписание включения:\n"
- for task in boot_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание включения: Не настроено\n"
-
- reply_text += "\n"
-
- if shutdown_tasks:
- reply_text += "Расписание выключения:\n"
- for task in shutdown_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание выключения: Не настроено\n"
-
- # Добавляем кнопки для управления расписанием
- keyboard = [
- [
- InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"),
- InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown")
- ],
- [
- InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
-
-async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /browse для просмотра файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем путь из аргументов команды или используем корневую директорию
- path = " ".join(context.args) if context.args else ""
-
- message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /search для поиска файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем шаблон поиска из аргументов команды
- if not context.args:
- await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>")
- return
-
- pattern = " ".join(context.args)
-
- message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
- return
-
- try:
- search_result = synology_api.search_files(pattern=pattern, limit=20)
-
- if not search_result.get("success", False):
- error = search_result.get("error", "unknown")
- progress = search_result.get("progress", 0)
-
- if error == "search_timeout":
- await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
- else:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- files = search_result.get("results", [])
- total = search_result.get("total", len(files))
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение с результатами поиска
- reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n"
-
- if not files:
- reply_text += "📭 Файлы не найдены"
- else:
- # Сортируем: сначала папки, потом файлы
- folders = []
- found_files = []
-
- for item in files:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path))
- else:
- # Для файлов получаем размер и путь к родительской папке
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- parent_path = "/".join(path.split("/")[:-1])
- found_files.append((name, path, size_str, parent_path))
-
- # Добавляем папки в сообщение
- if folders:
- reply_text += "Найденные папки:\n"
- for name, path in folders[:5]: # Показываем первые 5 папок
- reply_text += f"📁 {name}\n"
-
- if len(folders) > 5:
- reply_text += f"...и еще {len(folders) - 5} папок\n"
-
- reply_text += "\n"
-
- # Добавляем файлы в сообщение
- if found_files:
- reply_text += "Найденные файлы:\n"
- for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
- reply_text += f" Путь: .../{path.split('/')[-2]}/\n"
-
- if len(found_files) > 10:
- reply_text += f"...и еще {len(found_files) - 10} файлов\n"
-
- # Добавляем информацию о общем количестве результатов
- reply_text += f"\nВсего найдено: {total} элементов"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /updates для проверки обновлений"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
- return
-
- try:
- update_info = synology_api.check_for_updates()
-
- if not update_info.get("success", False):
- error = update_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML")
- return
-
- current_version = update_info.get("current_version", "unknown")
- update_available = update_info.get("update_available", False)
- auto_update = update_info.get("auto_update_enabled", False)
- updates = update_info.get("updates", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об обновлениях
- if update_available:
- reply_text = f"🔄 Доступны обновления DSM\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
- reply_text += "Доступные обновления:\n"
-
- for update_item in updates:
- update_name = update_item.get("name", "unknown")
- update_version = update_item.get("version", "unknown")
- update_size = update_item.get("size", 0)
- update_size_str = format_size(update_size)
-
- reply_text += f"• {update_name} v{update_version}\n"
- reply_text += f" └ Размер: {update_size_str}\n"
- else:
- reply_text = f"✅ Система в актуальном состоянии\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /backup для управления резервным копированием"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
- return
-
- try:
- backup_status = synology_api.get_backup_status()
-
- if not backup_status.get("success", False):
- error = backup_status.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML")
- return
-
- backups = backup_status.get("backups", {})
- api_status = backup_status.get("available_apis", {})
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о резервном копировании
- reply_text = f"💾 Резервное копирование Synology NAS\n\n"
-
- # Информация о Hyper Backup
- hyper_backups = backups.get("hyper_backup", [])
- hyper_api_available = api_status.get("hyper_backup", False)
-
- if hyper_api_available:
- reply_text += "Hyper Backup:\n"
-
- if hyper_backups:
- for backup in hyper_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
- last_backup = backup.get("last_backup", "never")
-
- status_emoji = "✅" if status.lower() == "success" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- reply_text += f" └ Последнее копирование: {last_backup}\n"
- else:
- reply_text += "Задачи Hyper Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о Time Backup
- time_backups = backups.get("time_backup", [])
- time_api_available = api_status.get("time_backup", False)
-
- if time_api_available:
- reply_text += "Time Backup:\n"
-
- if time_backups:
- for backup in time_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
-
- status_emoji = "✅" if status.lower() == "normal" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- else:
- reply_text += "Задачи Time Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о USB Copy
- usb_copy = backups.get("usb_copy", {})
- usb_api_available = api_status.get("usb_copy", False)
-
- if usb_api_available:
- usb_enabled = usb_copy.get("enabled", False)
- usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
-
- reply_text += f"USB Copy: {usb_status}\n\n"
-
- # Если ни один из API не доступен
- if not any(api_status.values()):
- reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /reboot для перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед перезагрузкой
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n"
- "Это действие может привести к прерыванию работы всех сервисов.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /sleep для перевода NAS в спящий режим"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед отправкой в спящий режим
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n"
- "Это действие приведет к остановке всех сервисов и отключению NAS.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- # Выполняем перезагрузку
- result = synology_api.reboot_system()
-
- if result:
- # Формируем сообщение об успешной перезагрузке
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /wakeup для включения NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
-
- # Проверяем, не включен ли NAS уже
- if synology_api.is_online(force_check=True):
- await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
- return
-
- try:
- # Отправляем сигнал пробуждения
- result = synology_api.power_on()
-
- if result:
- # Формируем сообщение об успешном включении
- reply_text = "✅ Synology NAS успешно включен\n\n"
- reply_text += "NAS полностью готов к работе."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quota для просмотра информации о квотах"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
- return
-
- try:
- quota_info = synology_api.get_quota_info()
-
- if not quota_info.get("success", False):
- error = quota_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML")
- return
-
- user_quotas = quota_info.get("user_quotas", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о квотах
- reply_text = f"📊 Квоты пользователей Synology NAS\n\n"
-
- if not user_quotas:
- reply_text += "Квоты пользователей не настроены или недоступны"
- else:
- for user_quota in user_quotas:
- user = user_quota.get("user", "unknown")
- quotas = user_quota.get("quotas", [])
-
- if quotas:
- reply_text += f"Пользователь {user}:\n"
-
- for quota in quotas:
- volume = quota.get("volume_name", "unknown")
- limit = quota.get("limit", 0)
- used = quota.get("used", 0)
-
- # Переводим байты в ГБ
- limit_gb = limit / (1024**3) if limit > 0 else 0
- used_gb = used / (1024**3)
-
- # Рассчитываем процент использования
- if limit_gb > 0:
- usage_percent = (used_gb / limit_gb) * 100
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- else:
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
-
- reply_text += "\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления расписанием питания"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("schedule_"):
- action_type = action.split("_")[1]
-
- if action_type == "add_boot":
- # Логика добавления расписания включения
- # В реальном боте здесь будет диалог для настройки расписания
- await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "add_shutdown":
- # Логика добавления расписания выключения
- await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "delete":
- # Логика удаления расписания
- await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
-async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для навигации по файловой системе"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("browse_"):
- path = action[7:] # Убираем префикс "browse_"
-
- # Используем команду browse с указанным путем
- message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках (аналогично функции browse_command)
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action == "confirm_reboot":
- # Выполняем перезагрузку
- message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.reboot_system()
-
- if result:
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_reboot":
- # Отменяем перезагрузку
- await query.edit_message_text("✅ Перезагрузка отменена", parse_mode="HTML")
-
- elif action == "confirm_sleep":
- # Выполняем переход в спящий режим (выключение)
- message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS уже оффлайн\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.power_off()
-
- if result:
- reply_text = "💤 Synology NAS переведен в спящий режим\n\n"
- reply_text += "Для пробуждения используйте команду /wakeup"
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при переходе в спящий режим\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при переходе в спящий режим\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_sleep":
- # Отменяем переход в спящий режим
- await query.edit_message_text("✅ Переход в спящий режим отменен", parse_mode="HTML")
-
-# Вспомогательные функции
-
-def format_size(size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат"""
- if size_bytes < 1024:
- return f"{size_bytes} Б"
- elif size_bytes < 1024**2:
- return f"{size_bytes/1024:.1f} КБ"
- elif size_bytes < 1024**3:
- return f"{size_bytes/1024**2:.1f} МБ"
- else:
- return f"{size_bytes/1024**3:.1f} ГБ"
-
-def get_file_icon(filename: str) -> str:
- """Возвращает эмодзи-иконку в зависимости от типа файла"""
- extension = filename.lower().split('.')[-1] if '.' in filename else ''
-
- if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
- return "🖼️"
- elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
- return "🎬"
- elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
- return "🎵"
- elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
- return "📄"
- elif extension in ['xls', 'xlsx', 'csv']:
- return "📊"
- elif extension in ['ppt', 'pptx']:
- return "📑"
- elif extension in ['pdf']:
- return "📕"
- elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
- return "🗜️"
- elif extension in ['exe', 'msi']:
- return "⚙️"
- else:
- return "📄"
diff --git a/.history/src/handlers/advanced_handlers_20250830105155.py b/.history/src/handlers/advanced_handlers_20250830105155.py
deleted file mode 100644
index 465de38..0000000
--- a/.history/src/handlers/advanced_handlers_20250830105155.py
+++ /dev/null
@@ -1,982 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Расширенные обработчики команд для управления Synology NAS
-"""
-
-import logging
-from datetime import datetime
-from typing import List, Dict, Any
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /processes для получения списка активных процессов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
- return
-
- try:
- processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
-
- if not processes:
- await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о процессах
- reply_text = f"⚙️ Активные процессы Synology NAS\n\n"
-
- for process in processes:
- name = process.get("name", "unknown")
- pid = process.get("pid", "?")
- cpu_usage = process.get("cpu_usage", 0)
- memory_usage = process.get("memory_usage", 0)
-
- reply_text += f"• {name} (PID: {pid})\n"
- reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
-
- reply_text += f"\nПоказано {len(processes)} наиболее активных процессов"
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /network для получения информации о сетевых подключениях"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
- return
-
- try:
- network_status = synology_api.get_network_status()
-
- if not network_status:
- await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о сетевых интерфейсах
- interfaces = network_status.get("interfaces", [])
-
- reply_text = f"🌐 Сетевые подключения Synology NAS\n\n"
-
- for interface in interfaces:
- name = interface.get("id", "unknown")
- ip = interface.get("ip", "Нет данных")
- mac = interface.get("mac", "Нет данных")
- status = "Активен" if interface.get("status") else "Неактивен"
-
- # Информация о трафике
- rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
- tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- if rx_bytes > 0 or tx_bytes > 0:
- reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /temperature для мониторинга температуры"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о температуре...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
- return
-
- try:
- temp_status = synology_api.get_temperature_status()
-
- if not temp_status:
- await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о температуре
- system_temp = temp_status.get("system_temperature")
- disk_temps = temp_status.get("disk_temperatures", [])
- is_warning = temp_status.get("warning", False)
-
- # Выбор emoji в зависимости от температуры
- temp_emoji = "🔥" if is_warning else "🌡️"
-
- reply_text = f"{temp_emoji} Температура Synology NAS\n\n"
-
- if system_temp is not None:
- temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная"
- reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n"
-
- if disk_temps:
- reply_text += "Температура дисков:\n"
- for disk in disk_temps:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temperature", 0)
-
- disk_temp_emoji = "🔥" if temp > 45 else "✅"
- reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /schedule для управления расписанием питания"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
- return
-
- try:
- schedule = synology_api.get_power_schedule()
-
- # Проверяем, пустая ли структура расписания
- if not schedule:
- await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
-
- # Проверяем, содержит ли расписание хотя бы одну задачу
- boot_tasks = schedule.get("boot_tasks", [])
- shutdown_tasks = schedule.get("shutdown_tasks", [])
-
- if not boot_tasks and not shutdown_tasks:
- await message.edit_text("ℹ️ Расписание питания не настроено\n\nНа вашем устройстве отсутствует настроенное расписание включения и выключения, либо API не поддерживается.", parse_mode="HTML")
- return
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о расписании питания
- boot_tasks = schedule.get("boot_tasks", [])
- shutdown_tasks = schedule.get("shutdown_tasks", [])
-
- reply_text = f"⏱️ Расписание питания Synology NAS\n\n"
-
- if boot_tasks:
- reply_text += "Расписание включения:\n"
- for task in boot_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание включения: Не настроено\n"
-
- reply_text += "\n"
-
- if shutdown_tasks:
- reply_text += "Расписание выключения:\n"
- for task in shutdown_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание выключения: Не настроено\n"
-
- # Добавляем кнопки для управления расписанием
- keyboard = [
- [
- InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"),
- InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown")
- ],
- [
- InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
-
-async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /browse для просмотра файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем путь из аргументов команды или используем корневую директорию
- path = " ".join(context.args) if context.args else ""
-
- message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /search для поиска файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем шаблон поиска из аргументов команды
- if not context.args:
- await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>")
- return
-
- pattern = " ".join(context.args)
-
- message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
- return
-
- try:
- search_result = synology_api.search_files(pattern=pattern, limit=20)
-
- if not search_result.get("success", False):
- error = search_result.get("error", "unknown")
- progress = search_result.get("progress", 0)
-
- if error == "search_timeout":
- await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
- else:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- files = search_result.get("results", [])
- total = search_result.get("total", len(files))
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение с результатами поиска
- reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n"
-
- if not files:
- reply_text += "📭 Файлы не найдены"
- else:
- # Сортируем: сначала папки, потом файлы
- folders = []
- found_files = []
-
- for item in files:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path))
- else:
- # Для файлов получаем размер и путь к родительской папке
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- parent_path = "/".join(path.split("/")[:-1])
- found_files.append((name, path, size_str, parent_path))
-
- # Добавляем папки в сообщение
- if folders:
- reply_text += "Найденные папки:\n"
- for name, path in folders[:5]: # Показываем первые 5 папок
- reply_text += f"📁 {name}\n"
-
- if len(folders) > 5:
- reply_text += f"...и еще {len(folders) - 5} папок\n"
-
- reply_text += "\n"
-
- # Добавляем файлы в сообщение
- if found_files:
- reply_text += "Найденные файлы:\n"
- for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
- reply_text += f" Путь: .../{path.split('/')[-2]}/\n"
-
- if len(found_files) > 10:
- reply_text += f"...и еще {len(found_files) - 10} файлов\n"
-
- # Добавляем информацию о общем количестве результатов
- reply_text += f"\nВсего найдено: {total} элементов"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /updates для проверки обновлений"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
- return
-
- try:
- update_info = synology_api.check_for_updates()
-
- if not update_info.get("success", False):
- error = update_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML")
- return
-
- current_version = update_info.get("current_version", "unknown")
- update_available = update_info.get("update_available", False)
- auto_update = update_info.get("auto_update_enabled", False)
- updates = update_info.get("updates", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об обновлениях
- if update_available:
- reply_text = f"🔄 Доступны обновления DSM\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
- reply_text += "Доступные обновления:\n"
-
- for update_item in updates:
- update_name = update_item.get("name", "unknown")
- update_version = update_item.get("version", "unknown")
- update_size = update_item.get("size", 0)
- update_size_str = format_size(update_size)
-
- reply_text += f"• {update_name} v{update_version}\n"
- reply_text += f" └ Размер: {update_size_str}\n"
- else:
- reply_text = f"✅ Система в актуальном состоянии\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /backup для управления резервным копированием"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
- return
-
- try:
- backup_status = synology_api.get_backup_status()
-
- if not backup_status.get("success", False):
- error = backup_status.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML")
- return
-
- backups = backup_status.get("backups", {})
- api_status = backup_status.get("available_apis", {})
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о резервном копировании
- reply_text = f"💾 Резервное копирование Synology NAS\n\n"
-
- # Информация о Hyper Backup
- hyper_backups = backups.get("hyper_backup", [])
- hyper_api_available = api_status.get("hyper_backup", False)
-
- if hyper_api_available:
- reply_text += "Hyper Backup:\n"
-
- if hyper_backups:
- for backup in hyper_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
- last_backup = backup.get("last_backup", "never")
-
- status_emoji = "✅" if status.lower() == "success" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- reply_text += f" └ Последнее копирование: {last_backup}\n"
- else:
- reply_text += "Задачи Hyper Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о Time Backup
- time_backups = backups.get("time_backup", [])
- time_api_available = api_status.get("time_backup", False)
-
- if time_api_available:
- reply_text += "Time Backup:\n"
-
- if time_backups:
- for backup in time_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
-
- status_emoji = "✅" if status.lower() == "normal" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- else:
- reply_text += "Задачи Time Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о USB Copy
- usb_copy = backups.get("usb_copy", {})
- usb_api_available = api_status.get("usb_copy", False)
-
- if usb_api_available:
- usb_enabled = usb_copy.get("enabled", False)
- usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
-
- reply_text += f"USB Copy: {usb_status}\n\n"
-
- # Если ни один из API не доступен
- if not any(api_status.values()):
- reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /reboot для перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед перезагрузкой
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n"
- "Это действие может привести к прерыванию работы всех сервисов.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /sleep для перевода NAS в спящий режим"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед отправкой в спящий режим
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n"
- "Это действие приведет к остановке всех сервисов и отключению NAS.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- # Выполняем перезагрузку
- result = synology_api.reboot_system()
-
- if result:
- # Формируем сообщение об успешной перезагрузке
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /wakeup для включения NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
-
- # Проверяем, не включен ли NAS уже
- if synology_api.is_online(force_check=True):
- await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
- return
-
- try:
- # Отправляем сигнал пробуждения
- result = synology_api.power_on()
-
- if result:
- # Формируем сообщение об успешном включении
- reply_text = "✅ Synology NAS успешно включен\n\n"
- reply_text += "NAS полностью готов к работе."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quota для просмотра информации о квотах"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
- return
-
- try:
- quota_info = synology_api.get_quota_info()
-
- if not quota_info.get("success", False):
- error = quota_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML")
- return
-
- user_quotas = quota_info.get("user_quotas", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о квотах
- reply_text = f"📊 Квоты пользователей Synology NAS\n\n"
-
- if not user_quotas:
- reply_text += "Квоты пользователей не настроены или недоступны"
- else:
- for user_quota in user_quotas:
- user = user_quota.get("user", "unknown")
- quotas = user_quota.get("quotas", [])
-
- if quotas:
- reply_text += f"Пользователь {user}:\n"
-
- for quota in quotas:
- volume = quota.get("volume_name", "unknown")
- limit = quota.get("limit", 0)
- used = quota.get("used", 0)
-
- # Переводим байты в ГБ
- limit_gb = limit / (1024**3) if limit > 0 else 0
- used_gb = used / (1024**3)
-
- # Рассчитываем процент использования
- if limit_gb > 0:
- usage_percent = (used_gb / limit_gb) * 100
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- else:
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
-
- reply_text += "\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления расписанием питания"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("schedule_"):
- action_type = action.split("_")[1]
-
- if action_type == "add_boot":
- # Логика добавления расписания включения
- # В реальном боте здесь будет диалог для настройки расписания
- await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "add_shutdown":
- # Логика добавления расписания выключения
- await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "delete":
- # Логика удаления расписания
- await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
-async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для навигации по файловой системе"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("browse_"):
- path = action[7:] # Убираем префикс "browse_"
-
- # Используем команду browse с указанным путем
- message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках (аналогично функции browse_command)
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action == "confirm_reboot":
- # Выполняем перезагрузку
- message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.reboot_system()
-
- if result:
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_reboot":
- # Отменяем перезагрузку
- await query.edit_message_text("✅ Перезагрузка отменена", parse_mode="HTML")
-
- elif action == "confirm_sleep":
- # Выполняем переход в спящий режим (выключение)
- message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS уже оффлайн\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.power_off()
-
- if result:
- reply_text = "💤 Synology NAS переведен в спящий режим\n\n"
- reply_text += "Для пробуждения используйте команду /wakeup"
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при переходе в спящий режим\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при переходе в спящий режим\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_sleep":
- # Отменяем переход в спящий режим
- await query.edit_message_text("✅ Переход в спящий режим отменен", parse_mode="HTML")
-
-# Вспомогательные функции
-
-def format_size(size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат"""
- if size_bytes < 1024:
- return f"{size_bytes} Б"
- elif size_bytes < 1024**2:
- return f"{size_bytes/1024:.1f} КБ"
- elif size_bytes < 1024**3:
- return f"{size_bytes/1024**2:.1f} МБ"
- else:
- return f"{size_bytes/1024**3:.1f} ГБ"
-
-def get_file_icon(filename: str) -> str:
- """Возвращает эмодзи-иконку в зависимости от типа файла"""
- extension = filename.lower().split('.')[-1] if '.' in filename else ''
-
- if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
- return "🖼️"
- elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
- return "🎬"
- elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
- return "🎵"
- elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
- return "📄"
- elif extension in ['xls', 'xlsx', 'csv']:
- return "📊"
- elif extension in ['ppt', 'pptx']:
- return "📑"
- elif extension in ['pdf']:
- return "📕"
- elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
- return "🗜️"
- elif extension in ['exe', 'msi']:
- return "⚙️"
- else:
- return "📄"
diff --git a/.history/src/handlers/advanced_handlers_20250830105216.py b/.history/src/handlers/advanced_handlers_20250830105216.py
deleted file mode 100644
index 50bb0c7..0000000
--- a/.history/src/handlers/advanced_handlers_20250830105216.py
+++ /dev/null
@@ -1,980 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Расширенные обработчики команд для управления Synology NAS
-"""
-
-import logging
-from datetime import datetime
-from typing import List, Dict, Any
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /processes для получения списка активных процессов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
- return
-
- try:
- processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
-
- if not processes:
- await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о процессах
- reply_text = f"⚙️ Активные процессы Synology NAS\n\n"
-
- for process in processes:
- name = process.get("name", "unknown")
- pid = process.get("pid", "?")
- cpu_usage = process.get("cpu_usage", 0)
- memory_usage = process.get("memory_usage", 0)
-
- reply_text += f"• {name} (PID: {pid})\n"
- reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
-
- reply_text += f"\nПоказано {len(processes)} наиболее активных процессов"
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /network для получения информации о сетевых подключениях"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
- return
-
- try:
- network_status = synology_api.get_network_status()
-
- if not network_status:
- await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о сетевых интерфейсах
- interfaces = network_status.get("interfaces", [])
-
- reply_text = f"🌐 Сетевые подключения Synology NAS\n\n"
-
- for interface in interfaces:
- name = interface.get("id", "unknown")
- ip = interface.get("ip", "Нет данных")
- mac = interface.get("mac", "Нет данных")
- status = "Активен" if interface.get("status") else "Неактивен"
-
- # Информация о трафике
- rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
- tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- if rx_bytes > 0 or tx_bytes > 0:
- reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /temperature для мониторинга температуры"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о температуре...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
- return
-
- try:
- temp_status = synology_api.get_temperature_status()
-
- if not temp_status:
- await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о температуре
- system_temp = temp_status.get("system_temperature")
- disk_temps = temp_status.get("disk_temperatures", [])
- is_warning = temp_status.get("warning", False)
-
- # Выбор emoji в зависимости от температуры
- temp_emoji = "🔥" if is_warning else "🌡️"
-
- reply_text = f"{temp_emoji} Температура Synology NAS\n\n"
-
- if system_temp is not None:
- temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная"
- reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n"
-
- if disk_temps:
- reply_text += "Температура дисков:\n"
- for disk in disk_temps:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temperature", 0)
-
- disk_temp_emoji = "🔥" if temp > 45 else "✅"
- reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /schedule для управления расписанием питания"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
- return
-
- try:
- schedule = synology_api.get_power_schedule()
-
- # Проверяем, пустая ли структура расписания
- if not schedule:
- await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
-
- # Получаем задачи расписания
- boot_tasks = schedule.get("boot_tasks", [])
- shutdown_tasks = schedule.get("shutdown_tasks", [])
-
- if not boot_tasks and not shutdown_tasks:
- await message.edit_text("ℹ️ Расписание питания не настроено\n\nНа вашем устройстве отсутствует настроенное расписание включения и выключения, либо API не поддерживается.", parse_mode="HTML")
- return
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о расписании питания
-
- reply_text = f"⏱️ Расписание питания Synology NAS\n\n"
-
- if boot_tasks:
- reply_text += "Расписание включения:\n"
- for task in boot_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание включения: Не настроено\n"
-
- reply_text += "\n"
-
- if shutdown_tasks:
- reply_text += "Расписание выключения:\n"
- for task in shutdown_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание выключения: Не настроено\n"
-
- # Добавляем кнопки для управления расписанием
- keyboard = [
- [
- InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"),
- InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown")
- ],
- [
- InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
-
-async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /browse для просмотра файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем путь из аргументов команды или используем корневую директорию
- path = " ".join(context.args) if context.args else ""
-
- message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /search для поиска файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем шаблон поиска из аргументов команды
- if not context.args:
- await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>")
- return
-
- pattern = " ".join(context.args)
-
- message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
- return
-
- try:
- search_result = synology_api.search_files(pattern=pattern, limit=20)
-
- if not search_result.get("success", False):
- error = search_result.get("error", "unknown")
- progress = search_result.get("progress", 0)
-
- if error == "search_timeout":
- await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
- else:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- files = search_result.get("results", [])
- total = search_result.get("total", len(files))
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение с результатами поиска
- reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n"
-
- if not files:
- reply_text += "📭 Файлы не найдены"
- else:
- # Сортируем: сначала папки, потом файлы
- folders = []
- found_files = []
-
- for item in files:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path))
- else:
- # Для файлов получаем размер и путь к родительской папке
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- parent_path = "/".join(path.split("/")[:-1])
- found_files.append((name, path, size_str, parent_path))
-
- # Добавляем папки в сообщение
- if folders:
- reply_text += "Найденные папки:\n"
- for name, path in folders[:5]: # Показываем первые 5 папок
- reply_text += f"📁 {name}\n"
-
- if len(folders) > 5:
- reply_text += f"...и еще {len(folders) - 5} папок\n"
-
- reply_text += "\n"
-
- # Добавляем файлы в сообщение
- if found_files:
- reply_text += "Найденные файлы:\n"
- for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
- reply_text += f" Путь: .../{path.split('/')[-2]}/\n"
-
- if len(found_files) > 10:
- reply_text += f"...и еще {len(found_files) - 10} файлов\n"
-
- # Добавляем информацию о общем количестве результатов
- reply_text += f"\nВсего найдено: {total} элементов"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /updates для проверки обновлений"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
- return
-
- try:
- update_info = synology_api.check_for_updates()
-
- if not update_info.get("success", False):
- error = update_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML")
- return
-
- current_version = update_info.get("current_version", "unknown")
- update_available = update_info.get("update_available", False)
- auto_update = update_info.get("auto_update_enabled", False)
- updates = update_info.get("updates", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об обновлениях
- if update_available:
- reply_text = f"🔄 Доступны обновления DSM\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
- reply_text += "Доступные обновления:\n"
-
- for update_item in updates:
- update_name = update_item.get("name", "unknown")
- update_version = update_item.get("version", "unknown")
- update_size = update_item.get("size", 0)
- update_size_str = format_size(update_size)
-
- reply_text += f"• {update_name} v{update_version}\n"
- reply_text += f" └ Размер: {update_size_str}\n"
- else:
- reply_text = f"✅ Система в актуальном состоянии\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /backup для управления резервным копированием"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
- return
-
- try:
- backup_status = synology_api.get_backup_status()
-
- if not backup_status.get("success", False):
- error = backup_status.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML")
- return
-
- backups = backup_status.get("backups", {})
- api_status = backup_status.get("available_apis", {})
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о резервном копировании
- reply_text = f"💾 Резервное копирование Synology NAS\n\n"
-
- # Информация о Hyper Backup
- hyper_backups = backups.get("hyper_backup", [])
- hyper_api_available = api_status.get("hyper_backup", False)
-
- if hyper_api_available:
- reply_text += "Hyper Backup:\n"
-
- if hyper_backups:
- for backup in hyper_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
- last_backup = backup.get("last_backup", "never")
-
- status_emoji = "✅" if status.lower() == "success" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- reply_text += f" └ Последнее копирование: {last_backup}\n"
- else:
- reply_text += "Задачи Hyper Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о Time Backup
- time_backups = backups.get("time_backup", [])
- time_api_available = api_status.get("time_backup", False)
-
- if time_api_available:
- reply_text += "Time Backup:\n"
-
- if time_backups:
- for backup in time_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
-
- status_emoji = "✅" if status.lower() == "normal" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- else:
- reply_text += "Задачи Time Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о USB Copy
- usb_copy = backups.get("usb_copy", {})
- usb_api_available = api_status.get("usb_copy", False)
-
- if usb_api_available:
- usb_enabled = usb_copy.get("enabled", False)
- usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
-
- reply_text += f"USB Copy: {usb_status}\n\n"
-
- # Если ни один из API не доступен
- if not any(api_status.values()):
- reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /reboot для перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед перезагрузкой
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n"
- "Это действие может привести к прерыванию работы всех сервисов.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /sleep для перевода NAS в спящий режим"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед отправкой в спящий режим
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n"
- "Это действие приведет к остановке всех сервисов и отключению NAS.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- # Выполняем перезагрузку
- result = synology_api.reboot_system()
-
- if result:
- # Формируем сообщение об успешной перезагрузке
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /wakeup для включения NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
-
- # Проверяем, не включен ли NAS уже
- if synology_api.is_online(force_check=True):
- await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
- return
-
- try:
- # Отправляем сигнал пробуждения
- result = synology_api.power_on()
-
- if result:
- # Формируем сообщение об успешном включении
- reply_text = "✅ Synology NAS успешно включен\n\n"
- reply_text += "NAS полностью готов к работе."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quota для просмотра информации о квотах"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
- return
-
- try:
- quota_info = synology_api.get_quota_info()
-
- if not quota_info.get("success", False):
- error = quota_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML")
- return
-
- user_quotas = quota_info.get("user_quotas", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о квотах
- reply_text = f"📊 Квоты пользователей Synology NAS\n\n"
-
- if not user_quotas:
- reply_text += "Квоты пользователей не настроены или недоступны"
- else:
- for user_quota in user_quotas:
- user = user_quota.get("user", "unknown")
- quotas = user_quota.get("quotas", [])
-
- if quotas:
- reply_text += f"Пользователь {user}:\n"
-
- for quota in quotas:
- volume = quota.get("volume_name", "unknown")
- limit = quota.get("limit", 0)
- used = quota.get("used", 0)
-
- # Переводим байты в ГБ
- limit_gb = limit / (1024**3) if limit > 0 else 0
- used_gb = used / (1024**3)
-
- # Рассчитываем процент использования
- if limit_gb > 0:
- usage_percent = (used_gb / limit_gb) * 100
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- else:
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
-
- reply_text += "\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления расписанием питания"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("schedule_"):
- action_type = action.split("_")[1]
-
- if action_type == "add_boot":
- # Логика добавления расписания включения
- # В реальном боте здесь будет диалог для настройки расписания
- await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "add_shutdown":
- # Логика добавления расписания выключения
- await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "delete":
- # Логика удаления расписания
- await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
-async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для навигации по файловой системе"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("browse_"):
- path = action[7:] # Убираем префикс "browse_"
-
- # Используем команду browse с указанным путем
- message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках (аналогично функции browse_command)
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action == "confirm_reboot":
- # Выполняем перезагрузку
- message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.reboot_system()
-
- if result:
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_reboot":
- # Отменяем перезагрузку
- await query.edit_message_text("✅ Перезагрузка отменена", parse_mode="HTML")
-
- elif action == "confirm_sleep":
- # Выполняем переход в спящий режим (выключение)
- message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS уже оффлайн\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.power_off()
-
- if result:
- reply_text = "💤 Synology NAS переведен в спящий режим\n\n"
- reply_text += "Для пробуждения используйте команду /wakeup"
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при переходе в спящий режим\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при переходе в спящий режим\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_sleep":
- # Отменяем переход в спящий режим
- await query.edit_message_text("✅ Переход в спящий режим отменен", parse_mode="HTML")
-
-# Вспомогательные функции
-
-def format_size(size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат"""
- if size_bytes < 1024:
- return f"{size_bytes} Б"
- elif size_bytes < 1024**2:
- return f"{size_bytes/1024:.1f} КБ"
- elif size_bytes < 1024**3:
- return f"{size_bytes/1024**2:.1f} МБ"
- else:
- return f"{size_bytes/1024**3:.1f} ГБ"
-
-def get_file_icon(filename: str) -> str:
- """Возвращает эмодзи-иконку в зависимости от типа файла"""
- extension = filename.lower().split('.')[-1] if '.' in filename else ''
-
- if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
- return "🖼️"
- elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
- return "🎬"
- elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
- return "🎵"
- elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
- return "📄"
- elif extension in ['xls', 'xlsx', 'csv']:
- return "📊"
- elif extension in ['ppt', 'pptx']:
- return "📑"
- elif extension in ['pdf']:
- return "📕"
- elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
- return "🗜️"
- elif extension in ['exe', 'msi']:
- return "⚙️"
- else:
- return "📄"
diff --git a/.history/src/handlers/advanced_handlers_20250830110338.py b/.history/src/handlers/advanced_handlers_20250830110338.py
deleted file mode 100644
index 50bb0c7..0000000
--- a/.history/src/handlers/advanced_handlers_20250830110338.py
+++ /dev/null
@@ -1,980 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Расширенные обработчики команд для управления Synology NAS
-"""
-
-import logging
-from datetime import datetime
-from typing import List, Dict, Any
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /processes для получения списка активных процессов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML")
- return
-
- try:
- processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов
-
- if not processes:
- await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о процессах
- reply_text = f"⚙️ Активные процессы Synology NAS\n\n"
-
- for process in processes:
- name = process.get("name", "unknown")
- pid = process.get("pid", "?")
- cpu_usage = process.get("cpu_usage", 0)
- memory_usage = process.get("memory_usage", 0)
-
- reply_text += f"• {name} (PID: {pid})\n"
- reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n"
-
- reply_text += f"\nПоказано {len(processes)} наиболее активных процессов"
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /network для получения информации о сетевых подключениях"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML")
- return
-
- try:
- network_status = synology_api.get_network_status()
-
- if not network_status:
- await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о сетевых интерфейсах
- interfaces = network_status.get("interfaces", [])
-
- reply_text = f"🌐 Сетевые подключения Synology NAS\n\n"
-
- for interface in interfaces:
- name = interface.get("id", "unknown")
- ip = interface.get("ip", "Нет данных")
- mac = interface.get("mac", "Нет данных")
- status = "Активен" if interface.get("status") else "Неактивен"
-
- # Информация о трафике
- rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ
- tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- if rx_bytes > 0 or tx_bytes > 0:
- reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /temperature для мониторинга температуры"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о температуре...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML")
- return
-
- try:
- temp_status = synology_api.get_temperature_status()
-
- if not temp_status:
- await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о температуре
- system_temp = temp_status.get("system_temperature")
- disk_temps = temp_status.get("disk_temperatures", [])
- is_warning = temp_status.get("warning", False)
-
- # Выбор emoji в зависимости от температуры
- temp_emoji = "🔥" if is_warning else "🌡️"
-
- reply_text = f"{temp_emoji} Температура Synology NAS\n\n"
-
- if system_temp is not None:
- temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная"
- reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n"
-
- if disk_temps:
- reply_text += "Температура дисков:\n"
- for disk in disk_temps:
- name = disk.get("name", "unknown")
- model = disk.get("model", "unknown")
- temp = disk.get("temperature", 0)
-
- disk_temp_emoji = "🔥" if temp > 45 else "✅"
- reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /schedule для управления расписанием питания"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о расписании питания...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML")
- return
-
- try:
- schedule = synology_api.get_power_schedule()
-
- # Проверяем, пустая ли структура расписания
- if not schedule:
- await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
-
- # Получаем задачи расписания
- boot_tasks = schedule.get("boot_tasks", [])
- shutdown_tasks = schedule.get("shutdown_tasks", [])
-
- if not boot_tasks and not shutdown_tasks:
- await message.edit_text("ℹ️ Расписание питания не настроено\n\nНа вашем устройстве отсутствует настроенное расписание включения и выключения, либо API не поддерживается.", parse_mode="HTML")
- return
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о расписании питания
-
- reply_text = f"⏱️ Расписание питания Synology NAS\n\n"
-
- if boot_tasks:
- reply_text += "Расписание включения:\n"
- for task in boot_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание включения: Не настроено\n"
-
- reply_text += "\n"
-
- if shutdown_tasks:
- reply_text += "Расписание выключения:\n"
- for task in shutdown_tasks:
- days = task.get("day", [])
- time = task.get("time", "00:00")
- enabled = task.get("enabled", False)
-
- # Преобразуем номера дней в названия
- day_names = []
- for day in days:
- if day == 0: day_names.append("Пн")
- elif day == 1: day_names.append("Вт")
- elif day == 2: day_names.append("Ср")
- elif day == 3: day_names.append("Чт")
- elif day == 4: day_names.append("Пт")
- elif day == 5: day_names.append("Сб")
- elif day == 6: day_names.append("Вс")
-
- status = "✅ Активно" if enabled else "❌ Отключено"
- day_str = ", ".join(day_names) if day_names else "Нет дней"
-
- reply_text += f"• {status}: {time} ({day_str})\n"
- else:
- reply_text += "Расписание выключения: Не настроено\n"
-
- # Добавляем кнопки для управления расписанием
- keyboard = [
- [
- InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"),
- InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown")
- ],
- [
- InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
-
-async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /browse для просмотра файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем путь из аргументов команды или используем корневую директорию
- path = " ".join(context.args) if context.args else ""
-
- message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /search для поиска файлов"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Получаем шаблон поиска из аргументов команды
- if not context.args:
- await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>")
- return
-
- pattern = " ".join(context.args)
-
- message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML")
- return
-
- try:
- search_result = synology_api.search_files(pattern=pattern, limit=20)
-
- if not search_result.get("success", False):
- error = search_result.get("error", "unknown")
- progress = search_result.get("progress", 0)
-
- if error == "search_timeout":
- await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML")
- else:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- files = search_result.get("results", [])
- total = search_result.get("total", len(files))
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение с результатами поиска
- reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n"
-
- if not files:
- reply_text += "📭 Файлы не найдены"
- else:
- # Сортируем: сначала папки, потом файлы
- folders = []
- found_files = []
-
- for item in files:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path))
- else:
- # Для файлов получаем размер и путь к родительской папке
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- parent_path = "/".join(path.split("/")[:-1])
- found_files.append((name, path, size_str, parent_path))
-
- # Добавляем папки в сообщение
- if folders:
- reply_text += "Найденные папки:\n"
- for name, path in folders[:5]: # Показываем первые 5 папок
- reply_text += f"📁 {name}\n"
-
- if len(folders) > 5:
- reply_text += f"...и еще {len(folders) - 5} папок\n"
-
- reply_text += "\n"
-
- # Добавляем файлы в сообщение
- if found_files:
- reply_text += "Найденные файлы:\n"
- for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
- reply_text += f" Путь: .../{path.split('/')[-2]}/\n"
-
- if len(found_files) > 10:
- reply_text += f"...и еще {len(found_files) - 10} файлов\n"
-
- # Добавляем информацию о общем количестве результатов
- reply_text += f"\nВсего найдено: {total} элементов"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /updates для проверки обновлений"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных обновлений...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML")
- return
-
- try:
- update_info = synology_api.check_for_updates()
-
- if not update_info.get("success", False):
- error = update_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML")
- return
-
- current_version = update_info.get("current_version", "unknown")
- update_available = update_info.get("update_available", False)
- auto_update = update_info.get("auto_update_enabled", False)
- updates = update_info.get("updates", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об обновлениях
- if update_available:
- reply_text = f"🔄 Доступны обновления DSM\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n"
- reply_text += "Доступные обновления:\n"
-
- for update_item in updates:
- update_name = update_item.get("name", "unknown")
- update_version = update_item.get("version", "unknown")
- update_size = update_item.get("size", 0)
- update_size_str = format_size(update_size)
-
- reply_text += f"• {update_name} v{update_version}\n"
- reply_text += f" └ Размер: {update_size_str}\n"
- else:
- reply_text = f"✅ Система в актуальном состоянии\n\n"
- reply_text += f"Текущая версия: {current_version}\n"
- reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /backup для управления резервным копированием"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о резервном копировании...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML")
- return
-
- try:
- backup_status = synology_api.get_backup_status()
-
- if not backup_status.get("success", False):
- error = backup_status.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML")
- return
-
- backups = backup_status.get("backups", {})
- api_status = backup_status.get("available_apis", {})
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о резервном копировании
- reply_text = f"💾 Резервное копирование Synology NAS\n\n"
-
- # Информация о Hyper Backup
- hyper_backups = backups.get("hyper_backup", [])
- hyper_api_available = api_status.get("hyper_backup", False)
-
- if hyper_api_available:
- reply_text += "Hyper Backup:\n"
-
- if hyper_backups:
- for backup in hyper_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
- last_backup = backup.get("last_backup", "never")
-
- status_emoji = "✅" if status.lower() == "success" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- reply_text += f" └ Последнее копирование: {last_backup}\n"
- else:
- reply_text += "Задачи Hyper Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о Time Backup
- time_backups = backups.get("time_backup", [])
- time_api_available = api_status.get("time_backup", False)
-
- if time_api_available:
- reply_text += "Time Backup:\n"
-
- if time_backups:
- for backup in time_backups:
- name = backup.get("name", "unknown")
- status = backup.get("status", "unknown")
-
- status_emoji = "✅" if status.lower() == "normal" else "⚠️"
- reply_text += f"• {status_emoji} {name}\n"
- else:
- reply_text += "Задачи Time Backup не настроены\n"
-
- reply_text += "\n"
-
- # Информация о USB Copy
- usb_copy = backups.get("usb_copy", {})
- usb_api_available = api_status.get("usb_copy", False)
-
- if usb_api_available:
- usb_enabled = usb_copy.get("enabled", False)
- usb_status = "✅ Включено" if usb_enabled else "❌ Отключено"
-
- reply_text += f"USB Copy: {usb_status}\n\n"
-
- # Если ни один из API не доступен
- if not any(api_status.values()):
- reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /reboot для перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед перезагрузкой
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n"
- "Это действие может привести к прерыванию работы всех сервисов.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /sleep для перевода NAS в спящий режим"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- # Добавляем подтверждение перед отправкой в спящий режим
- keyboard = [
- [
- InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"),
- InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep")
- ]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await update.message.reply_text(
- "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n"
- "Это действие приведет к остановке всех сервисов и отключению NAS.",
- parse_mode="HTML",
- reply_markup=reply_markup
- )
-
-async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quickreboot для быстрой перезагрузки NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- # Выполняем перезагрузку
- result = synology_api.reboot_system()
-
- if result:
- # Формируем сообщение об успешной перезагрузке
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /wakeup для включения NAS"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...")
-
- # Проверяем, не включен ли NAS уже
- if synology_api.is_online(force_check=True):
- await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML")
- return
-
- try:
- # Отправляем сигнал пробуждения
- result = synology_api.power_on()
-
- if result:
- # Формируем сообщение об успешном включении
- reply_text = "✅ Synology NAS успешно включен\n\n"
- reply_text += "NAS полностью готов к работе."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML")
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
-async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /quota для просмотра информации о квотах"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML")
- return
-
- try:
- quota_info = synology_api.get_quota_info()
-
- if not quota_info.get("success", False):
- error = quota_info.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML")
- return
-
- user_quotas = quota_info.get("user_quotas", [])
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о квотах
- reply_text = f"📊 Квоты пользователей Synology NAS\n\n"
-
- if not user_quotas:
- reply_text += "Квоты пользователей не настроены или недоступны"
- else:
- for user_quota in user_quotas:
- user = user_quota.get("user", "unknown")
- quotas = user_quota.get("quotas", [])
-
- if quotas:
- reply_text += f"Пользователь {user}:\n"
-
- for quota in quotas:
- volume = quota.get("volume_name", "unknown")
- limit = quota.get("limit", 0)
- used = quota.get("used", 0)
-
- # Переводим байты в ГБ
- limit_gb = limit / (1024**3) if limit > 0 else 0
- used_gb = used / (1024**3)
-
- # Рассчитываем процент использования
- if limit_gb > 0:
- usage_percent = (used_gb / limit_gb) * 100
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- else:
- reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n"
-
- reply_text += "\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления расписанием питания"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("schedule_"):
- action_type = action.split("_")[1]
-
- if action_type == "add_boot":
- # Логика добавления расписания включения
- # В реальном боте здесь будет диалог для настройки расписания
- await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "add_shutdown":
- # Логика добавления расписания выключения
- await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
- elif action_type == "delete":
- # Логика удаления расписания
- await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML")
-
-async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для навигации по файловой системе"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action.startswith("browse_"):
- path = action[7:] # Убираем префикс "browse_"
-
- # Используем команду browse с указанным путем
- message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML")
- return
-
- try:
- browse_result = synology_api.browse_files(folder_path=path)
-
- if not browse_result.get("success", False):
- error = browse_result.get("error", "unknown")
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML")
- return
-
- items = browse_result.get("items", [])
- current_path = browse_result.get("path", "")
- is_root = browse_result.get("is_root", True)
-
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о файлах и папках (аналогично функции browse_command)
- if is_root:
- reply_text = f"📁 Общие папки Synology NAS\n\n"
- else:
- reply_text = f"📁 Содержимое папки\n{current_path}\n\n"
-
- # Сортируем: сначала папки, потом файлы
- folders = []
- files = []
-
- for item in items:
- if is_root: # Для корневого уровня все элементы - это общие папки
- name = item.get("name", "unknown")
- path = item.get("path", "")
- folders.append((name, path, True))
- else:
- name = item.get("name", "unknown")
- path = item.get("path", "")
- is_dir = item.get("isdir", False)
-
- if is_dir:
- folders.append((name, path, False))
- else:
- # Для файлов получаем размер
- size = item.get("additional", {}).get("size", 0)
- size_str = format_size(size)
- files.append((name, path, size_str))
-
- # Добавляем папки в сообщение
- if folders:
- for name, path, is_share in folders:
- # Для общих папок добавляем иконку дома
- icon = "🏠" if is_share else "📁"
- reply_text += f"{icon} {name}\n"
-
- # Добавляем файлы в сообщение
- if files:
- for name, path, size in files:
- # Выбираем иконку в зависимости от расширения
- icon = get_file_icon(name)
- reply_text += f"{icon} {name} ({size})\n"
-
- # Если нет элементов для отображения
- if not folders and not files:
- reply_text += "📭 Папка пуста\n"
-
- # Добавляем кнопку возврата наверх, если мы не в корне
- if not is_root:
- # Определяем родительскую директорию
- parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else ""
-
- keyboard = [
- [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")]
- ]
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup)
- else:
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = update.effective_user.id
- if user_id not in ADMIN_USER_IDS:
- await query.edit_message_text("У вас нет доступа к этому боту.")
- return
-
- action = query.data
-
- if action == "confirm_reboot":
- # Выполняем перезагрузку
- message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.reboot_system()
-
- if result:
- reply_text = "🔄 Synology NAS перезагружается\n\n"
- reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен."
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_reboot":
- # Отменяем перезагрузку
- await query.edit_message_text("✅ Перезагрузка отменена", parse_mode="HTML")
-
- elif action == "confirm_sleep":
- # Выполняем переход в спящий режим (выключение)
- message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS уже оффлайн\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML")
- return
-
- try:
- result = synology_api.power_off()
-
- if result:
- reply_text = "💤 Synology NAS переведен в спящий режим\n\n"
- reply_text += "Для пробуждения используйте команду /wakeup"
- await message.edit_text(reply_text, parse_mode="HTML")
- else:
- await message.edit_text("❌ Ошибка при переходе в спящий режим\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML")
- except Exception as e:
- await message.edit_text(f"❌ Ошибка при переходе в спящий режим\n\nПричина: {str(e)}", parse_mode="HTML")
-
- elif action == "cancel_sleep":
- # Отменяем переход в спящий режим
- await query.edit_message_text("✅ Переход в спящий режим отменен", parse_mode="HTML")
-
-# Вспомогательные функции
-
-def format_size(size_bytes: int) -> str:
- """Преобразует размер в байтах в человекочитаемый формат"""
- if size_bytes < 1024:
- return f"{size_bytes} Б"
- elif size_bytes < 1024**2:
- return f"{size_bytes/1024:.1f} КБ"
- elif size_bytes < 1024**3:
- return f"{size_bytes/1024**2:.1f} МБ"
- else:
- return f"{size_bytes/1024**3:.1f} ГБ"
-
-def get_file_icon(filename: str) -> str:
- """Возвращает эмодзи-иконку в зависимости от типа файла"""
- extension = filename.lower().split('.')[-1] if '.' in filename else ''
-
- if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']:
- return "🖼️"
- elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']:
- return "🎬"
- elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']:
- return "🎵"
- elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']:
- return "📄"
- elif extension in ['xls', 'xlsx', 'csv']:
- return "📊"
- elif extension in ['ppt', 'pptx']:
- return "📑"
- elif extension in ['pdf']:
- return "📕"
- elif extension in ['zip', 'rar', '7z', 'tar', 'gz']:
- return "🗜️"
- elif extension in ['exe', 'msi']:
- return "⚙️"
- else:
- return "📄"
diff --git a/.history/src/handlers/command_handlers_20250830063638.py b/.history/src/handlers/command_handlers_20250830063638.py
deleted file mode 100644
index 05c2c7d..0000000
--- a/.history/src/handlers/command_handlers_20250830063638.py
+++ /dev/null
@@ -1,275 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Помощь по использованию бота"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Доступные команды:\n\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info:
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version", "Неизвестная версия")
- uptime_seconds = system_info.get("uptime", 0)
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- await message.edit_text(
- "✅ Synology NAS онлайн\n\n"
- "Детальная информация недоступна. Возможно, необходимо авторизоваться.",
- parse_mode="HTML"
- )
- else:
- await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML")
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = [
- [
- InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else
- InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True)
- ],
- [
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True)
- ],
- [
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True)
- ],
- [InlineKeyboardButton("❌ Отмена", callback_data="cancel")]
- ]
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_off(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное выключение
- pass
- else:
- # Функция вернула False, ошибка выключения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power off: {str(e)}")
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830063839.py b/.history/src/handlers/command_handlers_20250830063839.py
deleted file mode 100644
index 05c2c7d..0000000
--- a/.history/src/handlers/command_handlers_20250830063839.py
+++ /dev/null
@@ -1,275 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Помощь по использованию бота"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Доступные команды:\n\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info:
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version", "Неизвестная версия")
- uptime_seconds = system_info.get("uptime", 0)
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- await message.edit_text(
- "✅ Synology NAS онлайн\n\n"
- "Детальная информация недоступна. Возможно, необходимо авторизоваться.",
- parse_mode="HTML"
- )
- else:
- await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML")
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = [
- [
- InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else
- InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True)
- ],
- [
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True)
- ],
- [
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True)
- ],
- [InlineKeyboardButton("❌ Отмена", callback_data="cancel")]
- ]
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_off(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное выключение
- pass
- else:
- # Функция вернула False, ошибка выключения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power off: {str(e)}")
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830065335.py b/.history/src/handlers/command_handlers_20250830065335.py
deleted file mode 100644
index d3bedbe..0000000
--- a/.history/src/handlers/command_handlers_20250830065335.py
+++ /dev/null
@@ -1,282 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Помощь по использованию бота"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info:
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version", "Неизвестная версия")
- uptime_seconds = system_info.get("uptime", 0)
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- await message.edit_text(
- "✅ Synology NAS онлайн\n\n"
- "Детальная информация недоступна. Возможно, необходимо авторизоваться.",
- parse_mode="HTML"
- )
- else:
- await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML")
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = [
- [
- InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else
- InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True)
- ],
- [
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True)
- ],
- [
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True)
- ],
- [InlineKeyboardButton("❌ Отмена", callback_data="cancel")]
- ]
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_off(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное выключение
- pass
- else:
- # Функция вернула False, ошибка выключения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power off: {str(e)}")
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830065348.py b/.history/src/handlers/command_handlers_20250830065348.py
deleted file mode 100644
index 35c3a90..0000000
--- a/.history/src/handlers/command_handlers_20250830065348.py
+++ /dev/null
@@ -1,286 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info:
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version", "Неизвестная версия")
- uptime_seconds = system_info.get("uptime", 0)
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- await message.edit_text(
- "✅ Synology NAS онлайн\n\n"
- "Детальная информация недоступна. Возможно, необходимо авторизоваться.",
- parse_mode="HTML"
- )
- else:
- await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML")
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = [
- [
- InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else
- InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True)
- ],
- [
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True)
- ],
- [
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True)
- ],
- [InlineKeyboardButton("❌ Отмена", callback_data="cancel")]
- ]
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_off(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное выключение
- pass
- else:
- # Функция вернула False, ошибка выключения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power off: {str(e)}")
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830065454.py b/.history/src/handlers/command_handlers_20250830065454.py
deleted file mode 100644
index 35c3a90..0000000
--- a/.history/src/handlers/command_handlers_20250830065454.py
+++ /dev/null
@@ -1,286 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info:
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version", "Неизвестная версия")
- uptime_seconds = system_info.get("uptime", 0)
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- await message.edit_text(
- "✅ Synology NAS онлайн\n\n"
- "Детальная информация недоступна. Возможно, необходимо авторизоваться.",
- parse_mode="HTML"
- )
- else:
- await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML")
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = [
- [
- InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else
- InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True)
- ],
- [
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True)
- ],
- [
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True)
- ],
- [InlineKeyboardButton("❌ Отмена", callback_data="cancel")]
- ]
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_off(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное выключение
- pass
- else:
- # Функция вернула False, ошибка выключения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power off: {str(e)}")
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830073032.py b/.history/src/handlers/command_handlers_20250830073032.py
deleted file mode 100644
index 82e77fe..0000000
--- a/.history/src/handlers/command_handlers_20250830073032.py
+++ /dev/null
@@ -1,293 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Детальная информация недоступна. Возможно, необходимо авторизоваться."
- f"{error_info}",
- parse_mode="HTML"
- )
- else:
- await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML")
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = [
- [
- InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else
- InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True)
- ],
- [
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True)
- ],
- [
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True)
- ],
- [InlineKeyboardButton("❌ Отмена", callback_data="cancel")]
- ]
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_off(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное выключение
- pass
- else:
- # Функция вернула False, ошибка выключения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power off: {str(e)}")
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830073043.py b/.history/src/handlers/command_handlers_20250830073043.py
deleted file mode 100644
index 82e77fe..0000000
--- a/.history/src/handlers/command_handlers_20250830073043.py
+++ /dev/null
@@ -1,293 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Детальная информация недоступна. Возможно, необходимо авторизоваться."
- f"{error_info}",
- parse_mode="HTML"
- )
- else:
- await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML")
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = [
- [
- InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else
- InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True)
- ],
- [
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else
- InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True)
- ],
- [
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else
- InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True)
- ],
- [InlineKeyboardButton("❌ Отмена", callback_data="cancel")]
- ]
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_off(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное выключение
- pass
- else:
- # Функция вернула False, ошибка выключения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power off: {str(e)}")
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830073339.py b/.history/src/handlers/command_handlers_20250830073339.py
deleted file mode 100644
index 4cf628a..0000000
--- a/.history/src/handlers/command_handlers_20250830073339.py
+++ /dev/null
@@ -1,300 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Детальная информация недоступна. Возможно, необходимо авторизоваться."
- f"{error_info}",
- parse_mode="HTML"
- )
- else:
- await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML")
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_off(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное выключение
- pass
- else:
- # Функция вернула False, ошибка выключения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power off: {str(e)}")
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830073407.py b/.history/src/handlers/command_handlers_20250830073407.py
deleted file mode 100644
index ecb77fb..0000000
--- a/.history/src/handlers/command_handlers_20250830073407.py
+++ /dev/null
@@ -1,311 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Детальная информация недоступна. Возможно, необходимо авторизоваться."
- f"{error_info}",
- parse_mode="HTML"
- )
- else:
- await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML")
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_off(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное выключение
- pass
- else:
- # Функция вернула False, ошибка выключения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power off: {str(e)}")
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830073425.py b/.history/src/handlers/command_handlers_20250830073425.py
deleted file mode 100644
index ecb77fb..0000000
--- a/.history/src/handlers/command_handlers_20250830073425.py
+++ /dev/null
@@ -1,311 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Детальная информация недоступна. Возможно, необходимо авторизоваться."
- f"{error_info}",
- parse_mode="HTML"
- )
- else:
- await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML")
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_off(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное выключение
- pass
- else:
- # Функция вернула False, ошибка выключения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power off: {str(e)}")
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830073858.py b/.history/src/handlers/command_handlers_20250830073858.py
deleted file mode 100644
index b2108a8..0000000
--- a/.history/src/handlers/command_handlers_20250830073858.py
+++ /dev/null
@@ -1,328 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Детальная информация недоступна. Возможно, необходимо авторизоваться."
- f"{error_info}",
- parse_mode="HTML"
- )
- else:
- await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML")
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_off(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное выключение
- pass
- else:
- # Функция вернула False, ошибка выключения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830073916.py b/.history/src/handlers/command_handlers_20250830073916.py
deleted file mode 100644
index 6af8fa1..0000000
--- a/.history/src/handlers/command_handlers_20250830073916.py
+++ /dev/null
@@ -1,327 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Детальная информация недоступна. Возможно, необходимо авторизоваться."
- f"{error_info}",
- parse_mode="HTML"
- )
- else:
- await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML")
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- try:
- success = await handle_power_off(query.message.chat_id, context)
- # Если handle_power_off уже отправил сообщение об успехе или ошибке,
- # дополнительных сообщений не требуется
- except Exception as e:
- logger.error(f"Exception in power_off callback: {str(e)}")
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830074106.py b/.history/src/handlers/command_handlers_20250830074106.py
deleted file mode 100644
index b9c0c32..0000000
--- a/.history/src/handlers/command_handlers_20250830074106.py
+++ /dev/null
@@ -1,380 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- try:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- # Проверяем порт и сеть
- network_info = ""
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- s.close()
- if result == 0:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт"
- else:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})"
- except Exception as e:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Устройство доступно по сети, но детальная информация через API недоступна. "
- f"Возможно, необходимо проверить учетные данные или права доступа."
- f"{error_info}"
- f"{network_info}",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Ошибка при получении информации: {str(e)[:100]}...\n\n"
- f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
- parse_mode="HTML"
- )
- else:
- # Устройство не в сети, проверим соседние порты для диагностики
- port_scan_info = ""
- try:
- for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, test_port))
- s.close()
- status = "открыт" if result == 0 else "закрыт"
- port_scan_info += f"Порт {test_port}: {status}\n"
-
- # Добавим информацию о MAC-адресе для WoL
- mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
-
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Информация о сети:\n"
- f"IP: {SYNOLOGY_HOST}\n"
- f"{port_scan_info}\n"
- f"{mac_info}\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- try:
- success = await handle_power_off(query.message.chat_id, context)
- # Если handle_power_off уже отправил сообщение об успехе или ошибке,
- # дополнительных сообщений не требуется
- except Exception as e:
- logger.error(f"Exception in power_off callback: {str(e)}")
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830074122.py b/.history/src/handlers/command_handlers_20250830074122.py
deleted file mode 100644
index 688dfc7..0000000
--- a/.history/src/handlers/command_handlers_20250830074122.py
+++ /dev/null
@@ -1,383 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-import socket
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import (
- ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
-)
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- try:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- # Проверяем порт и сеть
- network_info = ""
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- s.close()
- if result == 0:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт"
- else:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})"
- except Exception as e:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Устройство доступно по сети, но детальная информация через API недоступна. "
- f"Возможно, необходимо проверить учетные данные или права доступа."
- f"{error_info}"
- f"{network_info}",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Ошибка при получении информации: {str(e)[:100]}...\n\n"
- f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
- parse_mode="HTML"
- )
- else:
- # Устройство не в сети, проверим соседние порты для диагностики
- port_scan_info = ""
- try:
- for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, test_port))
- s.close()
- status = "открыт" if result == 0 else "закрыт"
- port_scan_info += f"Порт {test_port}: {status}\n"
-
- # Добавим информацию о MAC-адресе для WoL
- mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
-
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Информация о сети:\n"
- f"IP: {SYNOLOGY_HOST}\n"
- f"{port_scan_info}\n"
- f"{mac_info}\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- try:
- success = await handle_power_off(query.message.chat_id, context)
- # Если handle_power_off уже отправил сообщение об успехе или ошибке,
- # дополнительных сообщений не требуется
- except Exception as e:
- logger.error(f"Exception in power_off callback: {str(e)}")
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830074140.py b/.history/src/handlers/command_handlers_20250830074140.py
deleted file mode 100644
index 688dfc7..0000000
--- a/.history/src/handlers/command_handlers_20250830074140.py
+++ /dev/null
@@ -1,383 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-import socket
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import (
- ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
-)
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- try:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- # Проверяем порт и сеть
- network_info = ""
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- s.close()
- if result == 0:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт"
- else:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})"
- except Exception as e:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Устройство доступно по сети, но детальная информация через API недоступна. "
- f"Возможно, необходимо проверить учетные данные или права доступа."
- f"{error_info}"
- f"{network_info}",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Ошибка при получении информации: {str(e)[:100]}...\n\n"
- f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
- parse_mode="HTML"
- )
- else:
- # Устройство не в сети, проверим соседние порты для диагностики
- port_scan_info = ""
- try:
- for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, test_port))
- s.close()
- status = "открыт" if result == 0 else "закрыт"
- port_scan_info += f"Порт {test_port}: {status}\n"
-
- # Добавим информацию о MAC-адресе для WoL
- mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
-
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Информация о сети:\n"
- f"IP: {SYNOLOGY_HOST}\n"
- f"{port_scan_info}\n"
- f"{mac_info}\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- try:
- success = await handle_power_off(query.message.chat_id, context)
- # Если handle_power_off уже отправил сообщение об успехе или ошибке,
- # дополнительных сообщений не требуется
- except Exception as e:
- logger.error(f"Exception in power_off callback: {str(e)}")
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830083412.py b/.history/src/handlers/command_handlers_20250830083412.py
deleted file mode 100644
index 3722e31..0000000
--- a/.history/src/handlers/command_handlers_20250830083412.py
+++ /dev/null
@@ -1,385 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-import socket
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import (
- ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
-)
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Диагностика:\n"
- "/checkapi - Проверка доступных API Synology\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- try:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- # Проверяем порт и сеть
- network_info = ""
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- s.close()
- if result == 0:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт"
- else:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})"
- except Exception as e:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Устройство доступно по сети, но детальная информация через API недоступна. "
- f"Возможно, необходимо проверить учетные данные или права доступа."
- f"{error_info}"
- f"{network_info}",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Ошибка при получении информации: {str(e)[:100]}...\n\n"
- f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
- parse_mode="HTML"
- )
- else:
- # Устройство не в сети, проверим соседние порты для диагностики
- port_scan_info = ""
- try:
- for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, test_port))
- s.close()
- status = "открыт" if result == 0 else "закрыт"
- port_scan_info += f"Порт {test_port}: {status}\n"
-
- # Добавим информацию о MAC-адресе для WoL
- mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
-
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Информация о сети:\n"
- f"IP: {SYNOLOGY_HOST}\n"
- f"{port_scan_info}\n"
- f"{mac_info}\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- try:
- success = await handle_power_off(query.message.chat_id, context)
- # Если handle_power_off уже отправил сообщение об успехе или ошибке,
- # дополнительных сообщений не требуется
- except Exception as e:
- logger.error(f"Exception in power_off callback: {str(e)}")
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830083502.py b/.history/src/handlers/command_handlers_20250830083502.py
deleted file mode 100644
index 3722e31..0000000
--- a/.history/src/handlers/command_handlers_20250830083502.py
+++ /dev/null
@@ -1,385 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-import socket
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import (
- ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
-)
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /start"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- logger.warning(f"Unauthorized access attempt from user ID: {user_id}")
- return
-
- await update.message.reply_text(
- f"Привет, {update.effective_user.first_name}! 👋\n\n"
- "Я бот для управления вашим Synology NAS.\n"
- "Используйте следующие команды:\n\n"
- "Основные команды:\n"
- "/status - Проверка статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/system - Информация о системе\n"
- "/storage - Информация о хранилище\n\n"
- "Используйте /help для получения полного списка команд",
- parse_mode="HTML"
- )
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /help"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- await update.message.reply_text(
- "📖 Помощь по использованию бота\n\n"
- "Основные команды:\n"
- "/start - Начало работы с ботом\n"
- "/status - Проверка текущего статуса NAS\n"
- "/power - Управление питанием NAS\n"
- "/help - Вывод этой справки\n\n"
- "Расширенные команды:\n"
- "/system - Подробная информация о системе\n"
- "/storage - Информация о хранилище и дисках\n"
- "/shares - Список общих папок\n"
- "/load - Текущая нагрузка на систему\n"
- "/security - Статус безопасности системы\n\n"
- "Диагностика:\n"
- "/checkapi - Проверка доступных API Synology\n\n"
- "Управление питанием:\n"
- "• Включение NAS: Wake-on-LAN\n"
- "• Выключение NAS: Безопасное завершение работы\n"
- "• Перезагрузка: Безопасная перезагрузка\n\n"
- "Примечание: Для работы функции включения необходимо, "
- "чтобы на NAS была настроена функция Wake-on-LAN.",
- parse_mode="HTML"
- )
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- try:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- # Проверяем порт и сеть
- network_info = ""
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- s.close()
- if result == 0:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт"
- else:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})"
- except Exception as e:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Устройство доступно по сети, но детальная информация через API недоступна. "
- f"Возможно, необходимо проверить учетные данные или права доступа."
- f"{error_info}"
- f"{network_info}",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Ошибка при получении информации: {str(e)[:100]}...\n\n"
- f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
- parse_mode="HTML"
- )
- else:
- # Устройство не в сети, проверим соседние порты для диагностики
- port_scan_info = ""
- try:
- for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, test_port))
- s.close()
- status = "открыт" if result == 0 else "закрыт"
- port_scan_info += f"Порт {test_port}: {status}\n"
-
- # Добавим информацию о MAC-адресе для WoL
- mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
-
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Информация о сети:\n"
- f"IP: {SYNOLOGY_HOST}\n"
- f"{port_scan_info}\n"
- f"{mac_info}\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- try:
- success = await handle_power_off(query.message.chat_id, context)
- # Если handle_power_off уже отправил сообщение об успехе или ошибке,
- # дополнительных сообщений не требуется
- except Exception as e:
- logger.error(f"Exception in power_off callback: {str(e)}")
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830092806.py b/.history/src/handlers/command_handlers_20250830092806.py
deleted file mode 100644
index 1b50d8e..0000000
--- a/.history/src/handlers/command_handlers_20250830092806.py
+++ /dev/null
@@ -1,331 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-import socket
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import (
- ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
-)
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- try:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- # Проверяем порт и сеть
- network_info = ""
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- s.close()
- if result == 0:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт"
- else:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})"
- except Exception as e:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Устройство доступно по сети, но детальная информация через API недоступна. "
- f"Возможно, необходимо проверить учетные данные или права доступа."
- f"{error_info}"
- f"{network_info}",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Ошибка при получении информации: {str(e)[:100]}...\n\n"
- f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
- parse_mode="HTML"
- )
- else:
- # Устройство не в сети, проверим соседние порты для диагностики
- port_scan_info = ""
- try:
- for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, test_port))
- s.close()
- status = "открыт" if result == 0 else "закрыт"
- port_scan_info += f"Порт {test_port}: {status}\n"
-
- # Добавим информацию о MAC-адресе для WoL
- mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
-
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Информация о сети:\n"
- f"IP: {SYNOLOGY_HOST}\n"
- f"{port_scan_info}\n"
- f"{mac_info}\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- try:
- success = await handle_power_off(query.message.chat_id, context)
- # Если handle_power_off уже отправил сообщение об успехе или ошибке,
- # дополнительных сообщений не требуется
- except Exception as e:
- logger.error(f"Exception in power_off callback: {str(e)}")
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830094738.py b/.history/src/handlers/command_handlers_20250830094738.py
deleted file mode 100644
index 1b50d8e..0000000
--- a/.history/src/handlers/command_handlers_20250830094738.py
+++ /dev/null
@@ -1,331 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-import socket
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import (
- ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
-)
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- try:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- # Проверяем порт и сеть
- network_info = ""
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- s.close()
- if result == 0:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт"
- else:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})"
- except Exception as e:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Устройство доступно по сети, но детальная информация через API недоступна. "
- f"Возможно, необходимо проверить учетные данные или права доступа."
- f"{error_info}"
- f"{network_info}",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Ошибка при получении информации: {str(e)[:100]}...\n\n"
- f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
- parse_mode="HTML"
- )
- else:
- # Устройство не в сети, проверим соседние порты для диагностики
- port_scan_info = ""
- try:
- for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, test_port))
- s.close()
- status = "открыт" if result == 0 else "закрыт"
- port_scan_info += f"Порт {test_port}: {status}\n"
-
- # Добавим информацию о MAC-адресе для WoL
- mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
-
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Информация о сети:\n"
- f"IP: {SYNOLOGY_HOST}\n"
- f"{port_scan_info}\n"
- f"{mac_info}\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- try:
- success = await handle_power_off(query.message.chat_id, context)
- # Если handle_power_off уже отправил сообщение об успехе или ошибке,
- # дополнительных сообщений не требуется
- except Exception as e:
- logger.error(f"Exception in power_off callback: {str(e)}")
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830110734.py b/.history/src/handlers/command_handlers_20250830110734.py
deleted file mode 100644
index 3538a8c..0000000
--- a/.history/src/handlers/command_handlers_20250830110734.py
+++ /dev/null
@@ -1,328 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-import socket
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import (
- ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
-)
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-from src.utils.admin_utils import admin_required
-
-@admin_required
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- try:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- # Проверяем порт и сеть
- network_info = ""
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- s.close()
- if result == 0:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт"
- else:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})"
- except Exception as e:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Устройство доступно по сети, но детальная информация через API недоступна. "
- f"Возможно, необходимо проверить учетные данные или права доступа."
- f"{error_info}"
- f"{network_info}",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Ошибка при получении информации: {str(e)[:100]}...\n\n"
- f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
- parse_mode="HTML"
- )
- else:
- # Устройство не в сети, проверим соседние порты для диагностики
- port_scan_info = ""
- try:
- for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, test_port))
- s.close()
- status = "открыт" if result == 0 else "закрыт"
- port_scan_info += f"Порт {test_port}: {status}\n"
-
- # Добавим информацию о MAC-адресе для WoL
- mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
-
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Информация о сети:\n"
- f"IP: {SYNOLOGY_HOST}\n"
- f"{port_scan_info}\n"
- f"{mac_info}\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- try:
- success = await handle_power_off(query.message.chat_id, context)
- # Если handle_power_off уже отправил сообщение об успехе или ошибке,
- # дополнительных сообщений не требуется
- except Exception as e:
- logger.error(f"Exception in power_off callback: {str(e)}")
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830110754.py b/.history/src/handlers/command_handlers_20250830110754.py
deleted file mode 100644
index 4346748..0000000
--- a/.history/src/handlers/command_handlers_20250830110754.py
+++ /dev/null
@@ -1,329 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-import socket
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import (
- ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
-)
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-from src.utils.admin_utils import admin_required
-
-@admin_required
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- try:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- # Проверяем порт и сеть
- network_info = ""
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- s.close()
- if result == 0:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт"
- else:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})"
- except Exception as e:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Устройство доступно по сети, но детальная информация через API недоступна. "
- f"Возможно, необходимо проверить учетные данные или права доступа."
- f"{error_info}"
- f"{network_info}",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Ошибка при получении информации: {str(e)[:100]}...\n\n"
- f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
- parse_mode="HTML"
- )
- else:
- # Устройство не в сети, проверим соседние порты для диагностики
- port_scan_info = ""
- try:
- for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, test_port))
- s.close()
- status = "открыт" if result == 0 else "закрыт"
- port_scan_info += f"Порт {test_port}: {status}\n"
-
- # Добавим информацию о MAC-адресе для WoL
- mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
-
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Информация о сети:\n"
- f"IP: {SYNOLOGY_HOST}\n"
- f"{port_scan_info}\n"
- f"{mac_info}\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
-
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- try:
- success = await handle_power_off(query.message.chat_id, context)
- # Если handle_power_off уже отправил сообщение об успехе или ошибке,
- # дополнительных сообщений не требуется
- except Exception as e:
- logger.error(f"Exception in power_off callback: {str(e)}")
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830110810.py b/.history/src/handlers/command_handlers_20250830110810.py
deleted file mode 100644
index 82b5a96..0000000
--- a/.history/src/handlers/command_handlers_20250830110810.py
+++ /dev/null
@@ -1,325 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-import socket
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import (
- ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
-)
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-from src.utils.admin_utils import admin_required
-
-@admin_required
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- try:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- # Проверяем порт и сеть
- network_info = ""
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- s.close()
- if result == 0:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт"
- else:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})"
- except Exception as e:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Устройство доступно по сети, но детальная информация через API недоступна. "
- f"Возможно, необходимо проверить учетные данные или права доступа."
- f"{error_info}"
- f"{network_info}",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Ошибка при получении информации: {str(e)[:100]}...\n\n"
- f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
- parse_mode="HTML"
- )
- else:
- # Устройство не в сети, проверим соседние порты для диагностики
- port_scan_info = ""
- try:
- for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, test_port))
- s.close()
- status = "открыт" if result == 0 else "закрыт"
- port_scan_info += f"Порт {test_port}: {status}\n"
-
- # Добавим информацию о MAC-адресе для WoL
- mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
-
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Информация о сети:\n"
- f"IP: {SYNOLOGY_HOST}\n"
- f"{port_scan_info}\n"
- f"{mac_info}\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
-
-@admin_required
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- user_id = query.from_user.id
- if user_id not in ADMIN_USER_IDS:
- return
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- try:
- success = await handle_power_off(query.message.chat_id, context)
- # Если handle_power_off уже отправил сообщение об успехе или ошибке,
- # дополнительных сообщений не требуется
- except Exception as e:
- logger.error(f"Exception in power_off callback: {str(e)}")
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830110839.py b/.history/src/handlers/command_handlers_20250830110839.py
deleted file mode 100644
index 68fa640..0000000
--- a/.history/src/handlers/command_handlers_20250830110839.py
+++ /dev/null
@@ -1,322 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-import socket
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import (
- ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
-)
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-from src.utils.admin_utils import admin_required
-
-@admin_required
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- try:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- # Проверяем порт и сеть
- network_info = ""
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- s.close()
- if result == 0:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт"
- else:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})"
- except Exception as e:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Устройство доступно по сети, но детальная информация через API недоступна. "
- f"Возможно, необходимо проверить учетные данные или права доступа."
- f"{error_info}"
- f"{network_info}",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Ошибка при получении информации: {str(e)[:100]}...\n\n"
- f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
- parse_mode="HTML"
- )
- else:
- # Устройство не в сети, проверим соседние порты для диагностики
- port_scan_info = ""
- try:
- for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, test_port))
- s.close()
- status = "открыт" if result == 0 else "закрыт"
- port_scan_info += f"Порт {test_port}: {status}\n"
-
- # Добавим информацию о MAC-адресе для WoL
- mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
-
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Информация о сети:\n"
- f"IP: {SYNOLOGY_HOST}\n"
- f"{port_scan_info}\n"
- f"{mac_info}\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
-
-@admin_required
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-@admin_required
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- try:
- success = await handle_power_off(query.message.chat_id, context)
- # Если handle_power_off уже отправил сообщение об успехе или ошибке,
- # дополнительных сообщений не требуется
- except Exception as e:
- logger.error(f"Exception in power_off callback: {str(e)}")
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/command_handlers_20250830110906.py b/.history/src/handlers/command_handlers_20250830110906.py
deleted file mode 100644
index 68fa640..0000000
--- a/.history/src/handlers/command_handlers_20250830110906.py
+++ /dev/null
@@ -1,322 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Обработчики команд для телеграм-бота
-"""
-
-import logging
-import socket
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import (
- ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC
-)
-from src.api.synology import SynologyAPI
-from src.utils.admin_utils import admin_required
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-from src.utils.admin_utils import admin_required
-
-@admin_required
-async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /status"""
- message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...")
-
- is_online = synology_api.is_online()
-
- if is_online:
- try:
- # Если NAS включен, попробуем получить дополнительную информацию
- system_info = synology_api.get_system_status()
-
- if system_info and system_info.get("status") != "error":
- model = system_info.get("model", "Неизвестная модель")
- version = system_info.get("version_string", system_info.get("version", "Неизвестная версия"))
- uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0))
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(int(uptime_seconds), 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Модель: {model}\n"
- f"Версия DSM: {version}\n"
- f"Время работы: {uptime_str}",
- parse_mode="HTML"
- )
- else:
- # Обработка возможной ошибки API
- error_info = ""
- if system_info and system_info.get("status") == "error":
- error_code = system_info.get("error_code", "неизвестно")
- error_info = f"\nКод ошибки API: {error_code}"
-
- # Проверяем порт и сеть
- network_info = ""
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- s.close()
- if result == 0:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт"
- else:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})"
- except Exception as e:
- network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}"
-
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Устройство доступно по сети, но детальная информация через API недоступна. "
- f"Возможно, необходимо проверить учетные данные или права доступа."
- f"{error_info}"
- f"{network_info}",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"✅ Synology NAS онлайн\n\n"
- f"Ошибка при получении информации: {str(e)[:100]}...\n\n"
- f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}",
- parse_mode="HTML"
- )
- else:
- # Устройство не в сети, проверим соседние порты для диагностики
- port_scan_info = ""
- try:
- for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(1)
- result = s.connect_ex((SYNOLOGY_HOST, test_port))
- s.close()
- status = "открыт" if result == 0 else "закрыт"
- port_scan_info += f"Порт {test_port}: {status}\n"
-
- # Добавим информацию о MAC-адресе для WoL
- mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен"
-
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Информация о сети:\n"
- f"IP: {SYNOLOGY_HOST}\n"
- f"{port_scan_info}\n"
- f"{mac_info}\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
- except Exception as e:
- await message.edit_text(
- f"❌ Synology NAS оффлайн\n\n"
- f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n"
- f"Используйте /power для отправки Wake-on-LAN пакета",
- parse_mode="HTML"
- )
-
-@admin_required
-async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /power"""
-
- is_online = synology_api.is_online()
-
- keyboard = []
-
- # Кнопка включения
- if not is_online:
- keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")])
- else:
- keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")])
-
- # Кнопка выключения
- if is_online:
- keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")])
- else:
- keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")])
-
- # Кнопка перезагрузки
- if is_online:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")])
- else:
- keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")])
-
- # Кнопка отмены
- keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")])
-
- reply_markup = InlineKeyboardMarkup(keyboard)
-
- status_text = "✅ Онлайн" if is_online else "❌ Оффлайн"
-
- await update.message.reply_text(
- f"Управление питанием Synology NAS\n\n"
- f"Текущий статус: {status_text}\n\n"
- f"Выберите действие:",
- reply_markup=reply_markup,
- parse_mode="HTML"
- )
-
-@admin_required
-async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик callback-запросов для кнопок управления питанием"""
- query = update.callback_query
- await query.answer()
-
- action = query.data
-
- if action == "cancel":
- await query.edit_message_text("❌ Действие отменено")
- return
-
- # Обработка неактивных кнопок
- if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]:
- if action == "power_on_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже включен")
- elif action == "power_off_no_op":
- await query.edit_message_text("ℹ️ Synology NAS уже выключен")
- else:
- await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство")
- return
-
- # Обработка основных действий
- if action == "power_on":
- await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...")
-
- if await context.application.create_task(
- handle_power_on(query.message.chat_id, context)
- ):
- # Функция вернула True, успешное включение
- pass
- else:
- # Функция вернула False, ошибка включения
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN."
- )
-
- elif action == "power_off":
- await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...")
-
- try:
- success = await handle_power_off(query.message.chat_id, context)
- # Если handle_power_off уже отправил сообщение об успехе или ошибке,
- # дополнительных сообщений не требуется
- except Exception as e:
- logger.error(f"Exception in power_off callback: {str(e)}")
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные."
- )
-
- elif action == "reboot":
- await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...")
-
- if await context.application.create_task(
- handle_reboot(query.message.chat_id, context)
- ):
- # Функция вернула True, успешная перезагрузка
- pass
- else:
- # Функция вернула False, ошибка перезагрузки
- await context.bot.send_message(
- chat_id=query.message.chat_id,
- text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные."
- )
-
-async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для включения NAS"""
- try:
- # Отправка запроса на включение
- success = synology_api.power_on()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно включен и доступен"
- )
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during power on: {str(e)}")
- return False
-
-async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для выключения NAS"""
- try:
- # Проверка доступности NAS
- if not synology_api.is_online():
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения."
- )
- return False
-
- # Отправка запроса на выключение
- success = synology_api.power_off()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда выключения успешно отправлена. Synology NAS выключается..."
- )
- return True
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации."
- )
- return False
- except Exception as e:
- error_msg = str(e)
- logger.error(f"Error during power off: {error_msg}")
- await context.bot.send_message(
- chat_id=chat_id,
- text=f"❌ Ошибка при выключении: {error_msg[:100]}..."
- )
- return False
-
-async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool:
- """Асинхронная функция для перезагрузки NAS"""
- try:
- # Отправка запроса на перезагрузку
- success = synology_api.reboot_system()
-
- if success:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..."
- )
-
- # Ждем некоторое время перед проверкой статуса
- await context.bot.send_message(
- chat_id=chat_id,
- text="⏳ Ожидание перезагрузки системы..."
- )
-
- # Создаем задачу для ожидания загрузки
- wait_successful = synology_api.wait_for_boot()
-
- if wait_successful:
- await context.bot.send_message(
- chat_id=chat_id,
- text="✅ Synology NAS успешно перезагружен и снова онлайн"
- )
- else:
- await context.bot.send_message(
- chat_id=chat_id,
- text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную."
- )
-
- return True
- else:
- return False
- except Exception as e:
- logger.error(f"Error during reboot: {str(e)}")
- return False
diff --git a/.history/src/handlers/extended_handlers_20250830065246.py b/.history/src/handlers/extended_handlers_20250830065246.py
deleted file mode 100644
index e7d707f..0000000
--- a/.history/src/handlers/extended_handlers_20250830065246.py
+++ /dev/null
@@ -1,255 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- total_size_gb = storage_info.get("total_size", 0) / (1024**3)
- total_used_gb = storage_info.get("total_used", 0) / (1024**3)
- usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size_gb = volume.get("total_size", 0) / (1024**3)
- used_gb = volume.get("used_size", 0) / (1024**3)
- percent = volume.get("percent_used", 0)
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830065455.py b/.history/src/handlers/extended_handlers_20250830065455.py
deleted file mode 100644
index e7d707f..0000000
--- a/.history/src/handlers/extended_handlers_20250830065455.py
+++ /dev/null
@@ -1,255 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- total_size_gb = storage_info.get("total_size", 0) / (1024**3)
- total_used_gb = storage_info.get("total_used", 0) / (1024**3)
- usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size_gb = volume.get("total_size", 0) / (1024**3)
- used_gb = volume.get("used_size", 0) / (1024**3)
- percent = volume.get("percent_used", 0)
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830073718.py b/.history/src/handlers/extended_handlers_20250830073718.py
deleted file mode 100644
index af0cd8d..0000000
--- a/.history/src/handlers/extended_handlers_20250830073718.py
+++ /dev/null
@@ -1,259 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- total_size_gb = storage_info.get("total_size", 0) / (1024**3)
- total_used_gb = storage_info.get("total_used", 0) / (1024**3)
- usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size_gb = volume.get("total_size", 0) / (1024**3)
- used_gb = volume.get("used_size", 0) / (1024**3)
- percent = volume.get("percent_used", 0)
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830073739.py b/.history/src/handlers/extended_handlers_20250830073739.py
deleted file mode 100644
index 447b046..0000000
--- a/.history/src/handlers/extended_handlers_20250830073739.py
+++ /dev/null
@@ -1,263 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- total_size_gb = storage_info.get("total_size", 0) / (1024**3)
- total_used_gb = storage_info.get("total_used", 0) / (1024**3)
- usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size_gb = volume.get("total_size", 0) / (1024**3)
- used_gb = volume.get("used_size", 0) / (1024**3)
- percent = volume.get("percent_used", 0)
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830073759.py b/.history/src/handlers/extended_handlers_20250830073759.py
deleted file mode 100644
index 65c5d7e..0000000
--- a/.history/src/handlers/extended_handlers_20250830073759.py
+++ /dev/null
@@ -1,273 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- total_size_gb = storage_info.get("total_size", 0) / (1024**3)
- total_used_gb = storage_info.get("total_used", 0) / (1024**3)
- usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size_gb = volume.get("total_size", 0) / (1024**3)
- used_gb = volume.get("used_size", 0) / (1024**3)
- percent = volume.get("percent_used", 0)
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830073819.py b/.history/src/handlers/extended_handlers_20250830073819.py
deleted file mode 100644
index e7834cb..0000000
--- a/.history/src/handlers/extended_handlers_20250830073819.py
+++ /dev/null
@@ -1,277 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- total_size_gb = storage_info.get("total_size", 0) / (1024**3)
- total_used_gb = storage_info.get("total_used", 0) / (1024**3)
- usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size_gb = volume.get("total_size", 0) / (1024**3)
- used_gb = volume.get("used_size", 0) / (1024**3)
- percent = volume.get("percent_used", 0)
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830073837.py b/.history/src/handlers/extended_handlers_20250830073837.py
deleted file mode 100644
index cf0b14a..0000000
--- a/.history/src/handlers/extended_handlers_20250830073837.py
+++ /dev/null
@@ -1,281 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- total_size_gb = storage_info.get("total_size", 0) / (1024**3)
- total_used_gb = storage_info.get("total_used", 0) / (1024**3)
- usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size_gb = volume.get("total_size", 0) / (1024**3)
- used_gb = volume.get("used_size", 0) / (1024**3)
- percent = volume.get("percent_used", 0)
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830074140.py b/.history/src/handlers/extended_handlers_20250830074140.py
deleted file mode 100644
index cf0b14a..0000000
--- a/.history/src/handlers/extended_handlers_20250830074140.py
+++ /dev/null
@@ -1,281 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- total_size_gb = storage_info.get("total_size", 0) / (1024**3)
- total_used_gb = storage_info.get("total_used", 0) / (1024**3)
- usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size_gb = volume.get("total_size", 0) / (1024**3)
- used_gb = volume.get("used_size", 0) / (1024**3)
- percent = volume.get("percent_used", 0)
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830083308.py b/.history/src/handlers/extended_handlers_20250830083308.py
deleted file mode 100644
index efa4fa9..0000000
--- a/.history/src/handlers/extended_handlers_20250830083308.py
+++ /dev/null
@@ -1,345 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /checkapi для диагностики проблем с API"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
-
- from src.api.api_discovery import discover_available_apis
- from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_SECURE,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API,
- SYNOLOGY_API_VERSION
- )
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- # Получаем список доступных API
- apis = discover_available_apis(base_url)
-
- if not apis:
- await message.edit_text(
- "❌ Не удалось получить список доступных API\n\n"
- "Проверьте доступность NAS и сетевое подключение.",
- parse_mode="HTML"
- )
- return
-
- # Поиск API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
-
- # Формируем рекомендации
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
-
- # Формируем текст отчета
- api_report = (
- f"✅ Найдено {len(apis)} доступных API\n\n"
- f"API для управления питанием:\n"
- f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
- f"API для информации о системе:\n"
- f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
- f"API для перезагрузки:\n"
- f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
- f"Рекомендуемые настройки:\n"
- f"Power API: {recommended_power_api}\n"
- f"Info API: {recommended_info_api}\n\n"
- f"Текущие настройки в конфигурации:\n"
- f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
- f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
- f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
- )
-
- await message.edit_text(api_report, parse_mode="HTML")
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- total_size_gb = storage_info.get("total_size", 0) / (1024**3)
- total_used_gb = storage_info.get("total_used", 0) / (1024**3)
- usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size_gb = volume.get("total_size", 0) / (1024**3)
- used_gb = volume.get("used_size", 0) / (1024**3)
- percent = volume.get("percent_used", 0)
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830083502.py b/.history/src/handlers/extended_handlers_20250830083502.py
deleted file mode 100644
index efa4fa9..0000000
--- a/.history/src/handlers/extended_handlers_20250830083502.py
+++ /dev/null
@@ -1,345 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /checkapi для диагностики проблем с API"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
-
- from src.api.api_discovery import discover_available_apis
- from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_SECURE,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API,
- SYNOLOGY_API_VERSION
- )
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- # Получаем список доступных API
- apis = discover_available_apis(base_url)
-
- if not apis:
- await message.edit_text(
- "❌ Не удалось получить список доступных API\n\n"
- "Проверьте доступность NAS и сетевое подключение.",
- parse_mode="HTML"
- )
- return
-
- # Поиск API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
-
- # Формируем рекомендации
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
-
- # Формируем текст отчета
- api_report = (
- f"✅ Найдено {len(apis)} доступных API\n\n"
- f"API для управления питанием:\n"
- f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
- f"API для информации о системе:\n"
- f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
- f"API для перезагрузки:\n"
- f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
- f"Рекомендуемые настройки:\n"
- f"Power API: {recommended_power_api}\n"
- f"Info API: {recommended_info_api}\n\n"
- f"Текущие настройки в конфигурации:\n"
- f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
- f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
- f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
- )
-
- await message.edit_text(api_report, parse_mode="HTML")
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- total_size_gb = storage_info.get("total_size", 0) / (1024**3)
- total_used_gb = storage_info.get("total_used", 0) / (1024**3)
- usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size_gb = volume.get("total_size", 0) / (1024**3)
- used_gb = volume.get("used_size", 0) / (1024**3)
- percent = volume.get("percent_used", 0)
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830095429.py b/.history/src/handlers/extended_handlers_20250830095429.py
deleted file mode 100644
index 36ac9ca..0000000
--- a/.history/src/handlers/extended_handlers_20250830095429.py
+++ /dev/null
@@ -1,349 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /checkapi для диагностики проблем с API"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
-
- from src.api.api_discovery import discover_available_apis
- from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_SECURE,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API,
- SYNOLOGY_API_VERSION
- )
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- # Получаем список доступных API
- apis = discover_available_apis(base_url)
-
- if not apis:
- await message.edit_text(
- "❌ Не удалось получить список доступных API\n\n"
- "Проверьте доступность NAS и сетевое подключение.",
- parse_mode="HTML"
- )
- return
-
- # Поиск API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
-
- # Формируем рекомендации
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
-
- # Формируем текст отчета
- api_report = (
- f"✅ Найдено {len(apis)} доступных API\n\n"
- f"API для управления питанием:\n"
- f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
- f"API для информации о системе:\n"
- f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
- f"API для перезагрузки:\n"
- f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
- f"Рекомендуемые настройки:\n"
- f"Power API: {recommended_power_api}\n"
- f"Info API: {recommended_info_api}\n\n"
- f"Текущие настройки в конфигурации:\n"
- f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
- f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
- f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
- )
-
- await message.edit_text(api_report, parse_mode="HTML")
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- summary = storage_info.get("summary", {})
- total_size_gb = summary.get("total_space_gb", 0)
- total_used_gb = summary.get("used_space_gb", 0)
- free_space_gb = summary.get("free_space_gb", 0)
- usage_percent = summary.get("usage_percent", 0)
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size = volume.get("size", 0)
- used_size = volume.get("used_size", 0)
- size_gb = size / (1024**3)
- used_gb = used_size / (1024**3)
- percent = round((used_size / size) * 100, 1) if size > 0 else 0
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830095445.py b/.history/src/handlers/extended_handlers_20250830095445.py
deleted file mode 100644
index 773bfa4..0000000
--- a/.history/src/handlers/extended_handlers_20250830095445.py
+++ /dev/null
@@ -1,352 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /checkapi для диагностики проблем с API"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
-
- from src.api.api_discovery import discover_available_apis
- from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_SECURE,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API,
- SYNOLOGY_API_VERSION
- )
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- # Получаем список доступных API
- apis = discover_available_apis(base_url)
-
- if not apis:
- await message.edit_text(
- "❌ Не удалось получить список доступных API\n\n"
- "Проверьте доступность NAS и сетевое подключение.",
- parse_mode="HTML"
- )
- return
-
- # Поиск API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
-
- # Формируем рекомендации
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
-
- # Формируем текст отчета
- api_report = (
- f"✅ Найдено {len(apis)} доступных API\n\n"
- f"API для управления питанием:\n"
- f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
- f"API для информации о системе:\n"
- f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
- f"API для перезагрузки:\n"
- f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
- f"Рекомендуемые настройки:\n"
- f"Power API: {recommended_power_api}\n"
- f"Info API: {recommended_info_api}\n\n"
- f"Текущие настройки в конфигурации:\n"
- f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
- f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
- f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
- )
-
- await message.edit_text(api_report, parse_mode="HTML")
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- summary = storage_info.get("summary", {})
- total_size_gb = summary.get("total_space_gb", 0)
- total_used_gb = summary.get("used_space_gb", 0)
- free_space_gb = summary.get("free_space_gb", 0)
- usage_percent = summary.get("usage_percent", 0)
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size = volume.get("size", 0)
- used_size = volume.get("used_size", 0)
- size_gb = size / (1024**3)
- used_gb = used_size / (1024**3)
- percent = round((used_size / size) * 100, 1) if size > 0 else 0
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830095502.py b/.history/src/handlers/extended_handlers_20250830095502.py
deleted file mode 100644
index 7c9a0a4..0000000
--- a/.history/src/handlers/extended_handlers_20250830095502.py
+++ /dev/null
@@ -1,355 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /checkapi для диагностики проблем с API"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
-
- from src.api.api_discovery import discover_available_apis
- from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_SECURE,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API,
- SYNOLOGY_API_VERSION
- )
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- # Получаем список доступных API
- apis = discover_available_apis(base_url)
-
- if not apis:
- await message.edit_text(
- "❌ Не удалось получить список доступных API\n\n"
- "Проверьте доступность NAS и сетевое подключение.",
- parse_mode="HTML"
- )
- return
-
- # Поиск API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
-
- # Формируем рекомендации
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
-
- # Формируем текст отчета
- api_report = (
- f"✅ Найдено {len(apis)} доступных API\n\n"
- f"API для управления питанием:\n"
- f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
- f"API для информации о системе:\n"
- f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
- f"API для перезагрузки:\n"
- f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
- f"Рекомендуемые настройки:\n"
- f"Power API: {recommended_power_api}\n"
- f"Info API: {recommended_info_api}\n\n"
- f"Текущие настройки в конфигурации:\n"
- f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
- f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
- f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
- )
-
- await message.edit_text(api_report, parse_mode="HTML")
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- summary = storage_info.get("summary", {})
- total_size_gb = summary.get("total_space_gb", 0)
- total_used_gb = summary.get("used_space_gb", 0)
- free_space_gb = summary.get("free_space_gb", 0)
- usage_percent = summary.get("usage_percent", 0)
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size = volume.get("size", 0)
- used_size = volume.get("used_size", 0)
- size_gb = size / (1024**3)
- used_gb = used_size / (1024**3)
- percent = round((used_size / size) * 100, 1) if size > 0 else 0
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830095518.py b/.history/src/handlers/extended_handlers_20250830095518.py
deleted file mode 100644
index 4fc38c8..0000000
--- a/.history/src/handlers/extended_handlers_20250830095518.py
+++ /dev/null
@@ -1,358 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /checkapi для диагностики проблем с API"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
-
- from src.api.api_discovery import discover_available_apis
- from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_SECURE,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API,
- SYNOLOGY_API_VERSION
- )
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- # Получаем список доступных API
- apis = discover_available_apis(base_url)
-
- if not apis:
- await message.edit_text(
- "❌ Не удалось получить список доступных API\n\n"
- "Проверьте доступность NAS и сетевое подключение.",
- parse_mode="HTML"
- )
- return
-
- # Поиск API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
-
- # Формируем рекомендации
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
-
- # Формируем текст отчета
- api_report = (
- f"✅ Найдено {len(apis)} доступных API\n\n"
- f"API для управления питанием:\n"
- f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
- f"API для информации о системе:\n"
- f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
- f"API для перезагрузки:\n"
- f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
- f"Рекомендуемые настройки:\n"
- f"Power API: {recommended_power_api}\n"
- f"Info API: {recommended_info_api}\n\n"
- f"Текущие настройки в конфигурации:\n"
- f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
- f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
- f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
- )
-
- await message.edit_text(api_report, parse_mode="HTML")
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- summary = storage_info.get("summary", {})
- total_size_gb = summary.get("total_space_gb", 0)
- total_used_gb = summary.get("used_space_gb", 0)
- free_space_gb = summary.get("free_space_gb", 0)
- usage_percent = summary.get("usage_percent", 0)
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size = volume.get("size", 0)
- used_size = volume.get("used_size", 0)
- size_gb = size / (1024**3)
- used_gb = used_size / (1024**3)
- percent = round((used_size / size) * 100, 1) if size > 0 else 0
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830095533.py b/.history/src/handlers/extended_handlers_20250830095533.py
deleted file mode 100644
index 44d29a0..0000000
--- a/.history/src/handlers/extended_handlers_20250830095533.py
+++ /dev/null
@@ -1,361 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /checkapi для диагностики проблем с API"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
-
- from src.api.api_discovery import discover_available_apis
- from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_SECURE,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API,
- SYNOLOGY_API_VERSION
- )
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- # Получаем список доступных API
- apis = discover_available_apis(base_url)
-
- if not apis:
- await message.edit_text(
- "❌ Не удалось получить список доступных API\n\n"
- "Проверьте доступность NAS и сетевое подключение.",
- parse_mode="HTML"
- )
- return
-
- # Поиск API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
-
- # Формируем рекомендации
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
-
- # Формируем текст отчета
- api_report = (
- f"✅ Найдено {len(apis)} доступных API\n\n"
- f"API для управления питанием:\n"
- f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
- f"API для информации о системе:\n"
- f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
- f"API для перезагрузки:\n"
- f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
- f"Рекомендуемые настройки:\n"
- f"Power API: {recommended_power_api}\n"
- f"Info API: {recommended_info_api}\n\n"
- f"Текущие настройки в конфигурации:\n"
- f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
- f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
- f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
- )
-
- await message.edit_text(api_report, parse_mode="HTML")
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- summary = storage_info.get("summary", {})
- total_size_gb = summary.get("total_space_gb", 0)
- total_used_gb = summary.get("used_space_gb", 0)
- free_space_gb = summary.get("free_space_gb", 0)
- usage_percent = summary.get("usage_percent", 0)
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size = volume.get("size", 0)
- used_size = volume.get("used_size", 0)
- size_gb = size / (1024**3)
- used_gb = used_size / (1024**3)
- percent = round((used_size / size) * 100, 1) if size > 0 else 0
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830095550.py b/.history/src/handlers/extended_handlers_20250830095550.py
deleted file mode 100644
index e8fe930..0000000
--- a/.history/src/handlers/extended_handlers_20250830095550.py
+++ /dev/null
@@ -1,364 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /checkapi для диагностики проблем с API"""
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
-
- from src.api.api_discovery import discover_available_apis
- from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_SECURE,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API,
- SYNOLOGY_API_VERSION
- )
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- # Получаем список доступных API
- apis = discover_available_apis(base_url)
-
- if not apis:
- await message.edit_text(
- "❌ Не удалось получить список доступных API\n\n"
- "Проверьте доступность NAS и сетевое подключение.",
- parse_mode="HTML"
- )
- return
-
- # Поиск API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
-
- # Формируем рекомендации
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
-
- # Формируем текст отчета
- api_report = (
- f"✅ Найдено {len(apis)} доступных API\n\n"
- f"API для управления питанием:\n"
- f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
- f"API для информации о системе:\n"
- f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
- f"API для перезагрузки:\n"
- f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
- f"Рекомендуемые настройки:\n"
- f"Power API: {recommended_power_api}\n"
- f"Info API: {recommended_info_api}\n\n"
- f"Текущие настройки в конфигурации:\n"
- f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
- f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
- f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
- )
-
- await message.edit_text(api_report, parse_mode="HTML")
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- summary = storage_info.get("summary", {})
- total_size_gb = summary.get("total_space_gb", 0)
- total_used_gb = summary.get("used_space_gb", 0)
- free_space_gb = summary.get("free_space_gb", 0)
- usage_percent = summary.get("usage_percent", 0)
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size = volume.get("size", 0)
- used_size = volume.get("used_size", 0)
- size_gb = size / (1024**3)
- used_gb = used_size / (1024**3)
- percent = round((used_size / size) * 100, 1) if size > 0 else 0
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830095606.py b/.history/src/handlers/extended_handlers_20250830095606.py
deleted file mode 100644
index ca60d52..0000000
--- a/.history/src/handlers/extended_handlers_20250830095606.py
+++ /dev/null
@@ -1,367 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /checkapi для диагностики проблем с API"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
-
- from src.api.api_discovery import discover_available_apis
- from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_SECURE,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API,
- SYNOLOGY_API_VERSION
- )
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- # Получаем список доступных API
- apis = discover_available_apis(base_url)
-
- if not apis:
- await message.edit_text(
- "❌ Не удалось получить список доступных API\n\n"
- "Проверьте доступность NAS и сетевое подключение.",
- parse_mode="HTML"
- )
- return
-
- # Поиск API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
-
- # Формируем рекомендации
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
-
- # Формируем текст отчета
- api_report = (
- f"✅ Найдено {len(apis)} доступных API\n\n"
- f"API для управления питанием:\n"
- f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
- f"API для информации о системе:\n"
- f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
- f"API для перезагрузки:\n"
- f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
- f"Рекомендуемые настройки:\n"
- f"Power API: {recommended_power_api}\n"
- f"Info API: {recommended_info_api}\n\n"
- f"Текущие настройки в конфигурации:\n"
- f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
- f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
- f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
- )
-
- await message.edit_text(api_report, parse_mode="HTML")
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- summary = storage_info.get("summary", {})
- total_size_gb = summary.get("total_space_gb", 0)
- total_used_gb = summary.get("used_space_gb", 0)
- free_space_gb = summary.get("free_space_gb", 0)
- usage_percent = summary.get("usage_percent", 0)
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size = volume.get("size", 0)
- used_size = volume.get("used_size", 0)
- size_gb = size / (1024**3)
- used_gb = used_size / (1024**3)
- percent = round((used_size / size) * 100, 1) if size > 0 else 0
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830095651.py b/.history/src/handlers/extended_handlers_20250830095651.py
deleted file mode 100644
index ca60d52..0000000
--- a/.history/src/handlers/extended_handlers_20250830095651.py
+++ /dev/null
@@ -1,367 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /checkapi для диагностики проблем с API"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
-
- from src.api.api_discovery import discover_available_apis
- from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_SECURE,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API,
- SYNOLOGY_API_VERSION
- )
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- # Получаем список доступных API
- apis = discover_available_apis(base_url)
-
- if not apis:
- await message.edit_text(
- "❌ Не удалось получить список доступных API\n\n"
- "Проверьте доступность NAS и сетевое подключение.",
- parse_mode="HTML"
- )
- return
-
- # Поиск API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
-
- # Формируем рекомендации
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
-
- # Формируем текст отчета
- api_report = (
- f"✅ Найдено {len(apis)} доступных API\n\n"
- f"API для управления питанием:\n"
- f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
- f"API для информации о системе:\n"
- f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
- f"API для перезагрузки:\n"
- f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
- f"Рекомендуемые настройки:\n"
- f"Power API: {recommended_power_api}\n"
- f"Info API: {recommended_info_api}\n\n"
- f"Текущие настройки в конфигурации:\n"
- f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
- f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
- f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
- )
-
- await message.edit_text(api_report, parse_mode="HTML")
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- summary = storage_info.get("summary", {})
- total_size_gb = summary.get("total_space_gb", 0)
- total_used_gb = summary.get("used_space_gb", 0)
- free_space_gb = summary.get("free_space_gb", 0)
- usage_percent = summary.get("usage_percent", 0)
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size = volume.get("size", 0)
- used_size = volume.get("used_size", 0)
- size_gb = size / (1024**3)
- used_gb = used_size / (1024**3)
- percent = round((used_size / size) * 100, 1) if size > 0 else 0
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", {})
- if network:
- reply_text += "Сетевая активность:\n"
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830104501.py b/.history/src/handlers/extended_handlers_20250830104501.py
deleted file mode 100644
index de55528..0000000
--- a/.history/src/handlers/extended_handlers_20250830104501.py
+++ /dev/null
@@ -1,378 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /checkapi для диагностики проблем с API"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
-
- from src.api.api_discovery import discover_available_apis
- from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_SECURE,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API,
- SYNOLOGY_API_VERSION
- )
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- # Получаем список доступных API
- apis = discover_available_apis(base_url)
-
- if not apis:
- await message.edit_text(
- "❌ Не удалось получить список доступных API\n\n"
- "Проверьте доступность NAS и сетевое подключение.",
- parse_mode="HTML"
- )
- return
-
- # Поиск API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
-
- # Формируем рекомендации
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
-
- # Формируем текст отчета
- api_report = (
- f"✅ Найдено {len(apis)} доступных API\n\n"
- f"API для управления питанием:\n"
- f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
- f"API для информации о системе:\n"
- f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
- f"API для перезагрузки:\n"
- f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
- f"Рекомендуемые настройки:\n"
- f"Power API: {recommended_power_api}\n"
- f"Info API: {recommended_info_api}\n\n"
- f"Текущие настройки в конфигурации:\n"
- f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
- f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
- f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
- )
-
- await message.edit_text(api_report, parse_mode="HTML")
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- summary = storage_info.get("summary", {})
- total_size_gb = summary.get("total_space_gb", 0)
- total_used_gb = summary.get("used_space_gb", 0)
- free_space_gb = summary.get("free_space_gb", 0)
- usage_percent = summary.get("usage_percent", 0)
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size = volume.get("size", 0)
- used_size = volume.get("used_size", 0)
- size_gb = size / (1024**3)
- used_gb = used_size / (1024**3)
- percent = round((used_size / size) * 100, 1) if size > 0 else 0
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", [])
- if network:
- reply_text += "Сетевая активность:\n"
- if isinstance(network, dict):
- # Если это словарь (старое API)
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
- elif isinstance(network, list):
- # Если это список (новое API)
- for interface in network:
- device = interface.get("device", "неизвестно")
- rx = int(interface.get("rx", 0)) / (1024**2) # МБ
- tx = int(interface.get("tx", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/extended_handlers_20250830104715.py b/.history/src/handlers/extended_handlers_20250830104715.py
deleted file mode 100644
index de55528..0000000
--- a/.history/src/handlers/extended_handlers_20250830104715.py
+++ /dev/null
@@ -1,378 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Дополнительные обработчики команд для телеграм-бота
-"""
-
-import logging
-from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
-from telegram.ext import ContextTypes
-
-from src.config.config import ADMIN_USER_IDS
-from src.api.synology import SynologyAPI
-
-logger = logging.getLogger(__name__)
-
-# Инициализация API Synology
-synology_api = SynologyAPI()
-
-async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /checkapi для диагностики проблем с API"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...")
-
- from src.api.api_discovery import discover_available_apis
- from src.config.config import (
- SYNOLOGY_HOST,
- SYNOLOGY_PORT,
- SYNOLOGY_SECURE,
- SYNOLOGY_POWER_API,
- SYNOLOGY_INFO_API,
- SYNOLOGY_API_VERSION
- )
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- # Получаем список доступных API
- apis = discover_available_apis(base_url)
-
- if not apis:
- await message.edit_text(
- "❌ Не удалось получить список доступных API\n\n"
- "Проверьте доступность NAS и сетевое подключение.",
- parse_mode="HTML"
- )
- return
-
- # Поиск API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()]
- reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])]
-
- # Формируем рекомендации
- recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System"
- recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info"
-
- # Формируем текст отчета
- api_report = (
- f"✅ Найдено {len(apis)} доступных API\n\n"
- f"API для управления питанием:\n"
- f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n"
- f"API для информации о системе:\n"
- f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n"
- f"API для перезагрузки:\n"
- f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n"
- f"Рекомендуемые настройки:\n"
- f"Power API: {recommended_power_api}\n"
- f"Info API: {recommended_info_api}\n\n"
- f"Текущие настройки в конфигурации:\n"
- f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n"
- f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n"
- f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}"
- )
-
- await message.edit_text(api_report, parse_mode="HTML")
-
-async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /storage"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о хранилище...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML")
- return
-
- try:
- storage_info = synology_api.get_storage_status()
-
- if not storage_info:
- await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии хранилища
- summary = storage_info.get("summary", {})
- total_size_gb = summary.get("total_space_gb", 0)
- total_used_gb = summary.get("used_space_gb", 0)
- free_space_gb = summary.get("free_space_gb", 0)
- usage_percent = summary.get("usage_percent", 0)
-
- reply_text = f"📊 Информация о хранилище Synology NAS\n\n"
- reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n"
- reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n"
- reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n"
-
- # Добавляем информацию о томах
- volumes = storage_info.get("volumes", [])
- if volumes:
- reply_text += "Тома:\n"
- for volume in volumes:
- name = volume.get("name", "Неизвестно")
- status = volume.get("status", "Неизвестно")
- size = volume.get("size", 0)
- used_size = volume.get("used_size", 0)
- size_gb = size / (1024**3)
- used_gb = used_size / (1024**3)
- percent = round((used_size / size) * 100, 1) if size > 0 else 0
-
- reply_text += f"• {name} ({status})\n"
- reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n"
-
- # Добавляем информацию о дисках
- disks = storage_info.get("disks", [])
- if disks:
- reply_text += "\nДиски:\n"
- for disk in disks:
- name = disk.get("name", "Неизвестно")
- model = disk.get("model", "Неизвестно")
- status = disk.get("status", "Неизвестно")
- temp = disk.get("temp", "?")
-
- reply_text += f"• {name} - {model}\n"
- reply_text += f" └ Статус: {status}, Температура: {temp}°C\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /shares"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации об общих папках...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML")
- return
-
- try:
- shares = synology_api.get_shared_folders()
-
- if not shares:
- await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение об общих папках
- reply_text = f"📁 Общие папки Synology NAS\n\n"
-
- for share in shares:
- name = share.get("name", "Неизвестно")
- path = share.get("path", "Неизвестно")
- desc = share.get("desc", "")
-
- reply_text += f"• {name}\n"
- reply_text += f" └ Путь: {path}\n"
-
- if desc:
- reply_text += f" └ Описание: {desc}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /system"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о системе...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML")
- return
-
- try:
- system_status = synology_api.get_system_status()
-
- if not system_status:
- await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML")
- return
-
- # Если получен статус с ошибкой
- if system_status.get("status") == "error":
- error_code = system_status.get("error_code", "неизвестно")
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о состоянии системы
- model = system_status.get("model", "Неизвестно")
- version = system_status.get("version", "Неизвестно")
- serial = system_status.get("serial", "Неизвестно")
- uptime_seconds = system_status.get("uptime", 0)
- temperature = system_status.get("temperature", "?")
-
- # Преобразование времени работы в удобочитаемый формат
- days, remainder = divmod(uptime_seconds, 86400)
- hours, remainder = divmod(remainder, 3600)
- minutes, seconds = divmod(remainder, 60)
- uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с"
-
- reply_text = f"🖥️ Информация о системе Synology NAS\n\n"
- reply_text += f"Модель: {model}\n"
- reply_text += f"Серийный номер: {serial}\n"
- reply_text += f"Версия DSM: {version}\n"
- reply_text += f"Время работы: {uptime_str}\n"
- reply_text += f"Температура: {temperature}°C\n\n"
-
- # Добавляем информацию о CPU и памяти
- memory = system_status.get("memory", {})
- total_memory_gb = memory.get("total_mb", 0) / 1024
- available_memory_gb = memory.get("available_mb", 0) / 1024
- memory_usage = memory.get("usage_percent", 0)
- cpu_usage = system_status.get("cpu_usage", 0)
-
- reply_text += f"Загрузка CPU: {cpu_usage}%\n"
- reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n"
- reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n"
-
- # Добавляем информацию о сетевых интерфейсах
- network_info = system_status.get("network", [])
- if network_info:
- reply_text += "Сетевые интерфейсы:\n"
- for interface in network_info:
- device = interface.get("device", "Неизвестно")
- ip = interface.get("ip", "Неизвестно")
- mac = interface.get("mac", "Неизвестно")
-
- reply_text += f"• {device}\n"
- reply_text += f" └ IP: {ip}, MAC: {mac}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /load"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML")
- return
-
- try:
- system_load = synology_api.get_system_load()
-
- if not system_load:
- await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о нагрузке системы
- cpu_load = system_load.get("cpu_load", 0)
- memory = system_load.get("memory", {})
- memory_usage = memory.get("usage_percent", 0)
-
- reply_text = f"📈 Текущая нагрузка Synology NAS\n\n"
- reply_text += f"Загрузка CPU: {cpu_load}%\n"
- reply_text += f"Загрузка памяти: {memory_usage}%\n\n"
-
- # Добавляем информацию о сетевой активности
- network = system_load.get("network", [])
- if network:
- reply_text += "Сетевая активность:\n"
- if isinstance(network, dict):
- # Если это словарь (старое API)
- for device, stats in network.items():
- rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ
- tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
- elif isinstance(network, list):
- # Если это список (новое API)
- for interface in network:
- device = interface.get("device", "неизвестно")
- rx = int(interface.get("rx", 0)) / (1024**2) # МБ
- tx = int(interface.get("tx", 0)) / (1024**2) # МБ
-
- reply_text += f"• {device}\n"
- reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
-
-async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Обработчик команды /security"""
- if not update.message or not update.effective_user:
- return
-
- user_id = update.effective_user.id
-
- if user_id not in ADMIN_USER_IDS:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- message = await update.message.reply_text("⏳ Получение информации о безопасности...")
-
- if not synology_api.is_online():
- await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML")
- return
-
- try:
- security_info = synology_api.get_security_status()
-
- if not security_info.get("success", False):
- await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML")
- return
- except Exception as e:
- await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML")
- return
-
- # Формируем сообщение о безопасности
- status = security_info.get("status", "unknown")
- is_secure = security_info.get("is_secure", False)
- last_check = security_info.get("last_check", "Неизвестно")
-
- status_emoji = "✅" if is_secure else "⚠️"
- status_text = "Безопасно" if is_secure else "Требуется внимание"
-
- reply_text = f"🔐 Статус безопасности Synology NAS\n\n"
- reply_text += f"Статус: {status_emoji} {status_text}\n"
- reply_text += f"Подробности: {status}\n"
- reply_text += f"Последняя проверка: {last_check}\n"
-
- await message.edit_text(reply_text, parse_mode="HTML")
diff --git a/.history/src/handlers/help_handlers_20250830091943.py b/.history/src/handlers/help_handlers_20250830091943.py
deleted file mode 100644
index 28d248b..0000000
--- a/.history/src/handlers/help_handlers_20250830091943.py
+++ /dev/null
@@ -1,82 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль с функциями для генерации справочных сообщений о командах бота
-"""
-
-import logging
-from telegram import Update
-from telegram.ext import ContextTypes
-from telegram.constants import ParseMode
-
-logger = logging.getLogger(__name__)
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /help - выводит справку по всем доступным командам
- """
- user_id = update.effective_user.id
- username = update.effective_user.username
-
- logger.info(f"User {user_id} (@{username}) requested help")
-
- help_text = (
- "🤖 Synology Power Control Bot\n\n"
- "БАЗОВЫЕ КОМАНДЫ:\n"
- "/status - Проверить состояние NAS\n"
- "/power - Управление питанием NAS (меню)\n"
- "/reboot - Перезагрузка NAS (с подтверждением)\n"
- "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
-
- "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n"
- "/system - Информация о системе\n"
- "/storage - Состояние хранилища\n"
- "/shares - Список общих папок\n"
- "/load - Нагрузка на систему\n"
- "/security - Информация о безопасности\n"
- "/temperature - Температура устройства\n"
- "/processes - Список активных процессов\n"
- "/network - Сетевая информация\n\n"
-
- "РАСШИРЕННЫЕ КОМАНДЫ:\n"
- "/schedule - Расписание питания\n"
- "/browse - Просмотр файлов\n"
- "/search <запрос> - Поиск файлов\n"
- "/updates - Проверка обновлений\n"
- "/backup - Статус резервного копирования\n"
- "/quota - Квоты пользователей\n\n"
-
- "БЫСТРЫЕ КОМАНДЫ:\n"
- "/quickreboot - Быстрая перезагрузка\n"
- "/wakeup - Пробуждение NAS (WOL)\n\n"
-
- "СЛУЖЕБНЫЕ КОМАНДЫ:\n"
- "/checkapi - Проверка API\n"
- "/help - Эта справка\n"
- )
-
- await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /start - приветствие и краткая информация
- """
- user_id = update.effective_user.id
- username = update.effective_user.username
-
- logger.info(f"User {user_id} (@{username}) started the bot")
-
- welcome_text = (
- "👋 Добро пожаловать в Synology Power Control Bot!\n\n"
- "С помощью этого бота вы можете управлять питанием вашего Synology NAS "
- "и получать различную информацию о его состоянии.\n\n"
- "Для просмотра списка доступных команд используйте /help\n\n"
- "Базовые команды:\n"
- "• /status - Проверить состояние NAS\n"
- "• /power - Управление питанием\n"
- "• /system - Информация о системе\n"
- "• /storage - Состояние хранилища"
- )
-
- await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)
diff --git a/.history/src/handlers/help_handlers_20250830091955.py b/.history/src/handlers/help_handlers_20250830091955.py
deleted file mode 100644
index 9c5a8d7..0000000
--- a/.history/src/handlers/help_handlers_20250830091955.py
+++ /dev/null
@@ -1,82 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль с функциями для генерации справочных сообщений о командах бота
-"""
-
-import logging
-from telegram import Update
-from telegram.ext import ContextTypes
-from telegram.constants import ParseMode
-
-logger = logging.getLogger(__name__)
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /help - выводит справку по всем доступным командам
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) requested help")
-
- help_text = (
- "🤖 Synology Power Control Bot\n\n"
- "БАЗОВЫЕ КОМАНДЫ:\n"
- "/status - Проверить состояние NAS\n"
- "/power - Управление питанием NAS (меню)\n"
- "/reboot - Перезагрузка NAS (с подтверждением)\n"
- "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
-
- "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n"
- "/system - Информация о системе\n"
- "/storage - Состояние хранилища\n"
- "/shares - Список общих папок\n"
- "/load - Нагрузка на систему\n"
- "/security - Информация о безопасности\n"
- "/temperature - Температура устройства\n"
- "/processes - Список активных процессов\n"
- "/network - Сетевая информация\n\n"
-
- "РАСШИРЕННЫЕ КОМАНДЫ:\n"
- "/schedule - Расписание питания\n"
- "/browse - Просмотр файлов\n"
- "/search <запрос> - Поиск файлов\n"
- "/updates - Проверка обновлений\n"
- "/backup - Статус резервного копирования\n"
- "/quota - Квоты пользователей\n\n"
-
- "БЫСТРЫЕ КОМАНДЫ:\n"
- "/quickreboot - Быстрая перезагрузка\n"
- "/wakeup - Пробуждение NAS (WOL)\n\n"
-
- "СЛУЖЕБНЫЕ КОМАНДЫ:\n"
- "/checkapi - Проверка API\n"
- "/help - Эта справка\n"
- )
-
- await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /start - приветствие и краткая информация
- """
- user_id = update.effective_user.id
- username = update.effective_user.username
-
- logger.info(f"User {user_id} (@{username}) started the bot")
-
- welcome_text = (
- "👋 Добро пожаловать в Synology Power Control Bot!\n\n"
- "С помощью этого бота вы можете управлять питанием вашего Synology NAS "
- "и получать различную информацию о его состоянии.\n\n"
- "Для просмотра списка доступных команд используйте /help\n\n"
- "Базовые команды:\n"
- "• /status - Проверить состояние NAS\n"
- "• /power - Управление питанием\n"
- "• /system - Информация о системе\n"
- "• /storage - Состояние хранилища"
- )
-
- await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)
diff --git a/.history/src/handlers/help_handlers_20250830092004.py b/.history/src/handlers/help_handlers_20250830092004.py
deleted file mode 100644
index 7d0f885..0000000
--- a/.history/src/handlers/help_handlers_20250830092004.py
+++ /dev/null
@@ -1,82 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль с функциями для генерации справочных сообщений о командах бота
-"""
-
-import logging
-from telegram import Update
-from telegram.ext import ContextTypes
-from telegram.constants import ParseMode
-
-logger = logging.getLogger(__name__)
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /help - выводит справку по всем доступным командам
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) requested help")
-
- help_text = (
- "🤖 Synology Power Control Bot\n\n"
- "БАЗОВЫЕ КОМАНДЫ:\n"
- "/status - Проверить состояние NAS\n"
- "/power - Управление питанием NAS (меню)\n"
- "/reboot - Перезагрузка NAS (с подтверждением)\n"
- "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
-
- "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n"
- "/system - Информация о системе\n"
- "/storage - Состояние хранилища\n"
- "/shares - Список общих папок\n"
- "/load - Нагрузка на систему\n"
- "/security - Информация о безопасности\n"
- "/temperature - Температура устройства\n"
- "/processes - Список активных процессов\n"
- "/network - Сетевая информация\n\n"
-
- "РАСШИРЕННЫЕ КОМАНДЫ:\n"
- "/schedule - Расписание питания\n"
- "/browse - Просмотр файлов\n"
- "/search <запрос> - Поиск файлов\n"
- "/updates - Проверка обновлений\n"
- "/backup - Статус резервного копирования\n"
- "/quota - Квоты пользователей\n\n"
-
- "БЫСТРЫЕ КОМАНДЫ:\n"
- "/quickreboot - Быстрая перезагрузка\n"
- "/wakeup - Пробуждение NAS (WOL)\n\n"
-
- "СЛУЖЕБНЫЕ КОМАНДЫ:\n"
- "/checkapi - Проверка API\n"
- "/help - Эта справка\n"
- )
-
- await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /start - приветствие и краткая информация
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) started the bot")
-
- welcome_text = (
- "👋 Добро пожаловать в Synology Power Control Bot!\n\n"
- "С помощью этого бота вы можете управлять питанием вашего Synology NAS "
- "и получать различную информацию о его состоянии.\n\n"
- "Для просмотра списка доступных команд используйте /help\n\n"
- "Базовые команды:\n"
- "• /status - Проверить состояние NAS\n"
- "• /power - Управление питанием\n"
- "• /system - Информация о системе\n"
- "• /storage - Состояние хранилища"
- )
-
- await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)
diff --git a/.history/src/handlers/help_handlers_20250830092014.py b/.history/src/handlers/help_handlers_20250830092014.py
deleted file mode 100644
index 3df863d..0000000
--- a/.history/src/handlers/help_handlers_20250830092014.py
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль с функциями для генерации справочных сообщений о командах бота
-"""
-
-import logging
-from telegram import Update
-from telegram.ext import ContextTypes
-from telegram.constants import ParseMode
-
-logger = logging.getLogger(__name__)
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /help - выводит справку по всем доступным командам
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) requested help")
-
- help_text = (
- "🤖 Synology Power Control Bot\n\n"
- "БАЗОВЫЕ КОМАНДЫ:\n"
- "/status - Проверить состояние NAS\n"
- "/power - Управление питанием NAS (меню)\n"
- "/reboot - Перезагрузка NAS (с подтверждением)\n"
- "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
-
- "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n"
- "/system - Информация о системе\n"
- "/storage - Состояние хранилища\n"
- "/shares - Список общих папок\n"
- "/load - Нагрузка на систему\n"
- "/security - Информация о безопасности\n"
- "/temperature - Температура устройства\n"
- "/processes - Список активных процессов\n"
- "/network - Сетевая информация\n\n"
-
- "РАСШИРЕННЫЕ КОМАНДЫ:\n"
- "/schedule - Расписание питания\n"
- "/browse - Просмотр файлов\n"
- "/search <запрос> - Поиск файлов\n"
- "/updates - Проверка обновлений\n"
- "/backup - Статус резервного копирования\n"
- "/quota - Квоты пользователей\n\n"
-
- "БЫСТРЫЕ КОМАНДЫ:\n"
- "/quickreboot - Быстрая перезагрузка\n"
- "/wakeup - Пробуждение NAS (WOL)\n\n"
-
- "СЛУЖЕБНЫЕ КОМАНДЫ:\n"
- "/checkapi - Проверка API\n"
- "/help - Эта справка\n"
- )
-
- if update.message:
- await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
- elif update.callback_query:
- await update.callback_query.message.reply_text(help_text, parse_mode=ParseMode.HTML)
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /start - приветствие и краткая информация
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) started the bot")
-
- welcome_text = (
- "👋 Добро пожаловать в Synology Power Control Bot!\n\n"
- "С помощью этого бота вы можете управлять питанием вашего Synology NAS "
- "и получать различную информацию о его состоянии.\n\n"
- "Для просмотра списка доступных команд используйте /help\n\n"
- "Базовые команды:\n"
- "• /status - Проверить состояние NAS\n"
- "• /power - Управление питанием\n"
- "• /system - Информация о системе\n"
- "• /storage - Состояние хранилища"
- )
-
- await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)
diff --git a/.history/src/handlers/help_handlers_20250830092029.py b/.history/src/handlers/help_handlers_20250830092029.py
deleted file mode 100644
index 34506f3..0000000
--- a/.history/src/handlers/help_handlers_20250830092029.py
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль с функциями для генерации справочных сообщений о командах бота
-"""
-
-import logging
-from telegram import Update
-from telegram.ext import ContextTypes
-from telegram.constants import ParseMode
-
-logger = logging.getLogger(__name__)
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /help - выводит справку по всем доступным командам
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) requested help")
-
- help_text = (
- "🤖 Synology Power Control Bot\n\n"
- "БАЗОВЫЕ КОМАНДЫ:\n"
- "/status - Проверить состояние NAS\n"
- "/power - Управление питанием NAS (меню)\n"
- "/reboot - Перезагрузка NAS (с подтверждением)\n"
- "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
-
- "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n"
- "/system - Информация о системе\n"
- "/storage - Состояние хранилища\n"
- "/shares - Список общих папок\n"
- "/load - Нагрузка на систему\n"
- "/security - Информация о безопасности\n"
- "/temperature - Температура устройства\n"
- "/processes - Список активных процессов\n"
- "/network - Сетевая информация\n\n"
-
- "РАСШИРЕННЫЕ КОМАНДЫ:\n"
- "/schedule - Расписание питания\n"
- "/browse - Просмотр файлов\n"
- "/search <запрос> - Поиск файлов\n"
- "/updates - Проверка обновлений\n"
- "/backup - Статус резервного копирования\n"
- "/quota - Квоты пользователей\n\n"
-
- "БЫСТРЫЕ КОМАНДЫ:\n"
- "/quickreboot - Быстрая перезагрузка\n"
- "/wakeup - Пробуждение NAS (WOL)\n\n"
-
- "СЛУЖЕБНЫЕ КОМАНДЫ:\n"
- "/checkapi - Проверка API\n"
- "/help - Эта справка\n"
- )
-
- if update.message:
- await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
- elif update.callback_query:
- await update.callback_query.answer()
- await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML)
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /start - приветствие и краткая информация
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) started the bot")
-
- welcome_text = (
- "👋 Добро пожаловать в Synology Power Control Bot!\n\n"
- "С помощью этого бота вы можете управлять питанием вашего Synology NAS "
- "и получать различную информацию о его состоянии.\n\n"
- "Для просмотра списка доступных команд используйте /help\n\n"
- "Базовые команды:\n"
- "• /status - Проверить состояние NAS\n"
- "• /power - Управление питанием\n"
- "• /system - Информация о системе\n"
- "• /storage - Состояние хранилища"
- )
-
- await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)
diff --git a/.history/src/handlers/help_handlers_20250830092040.py b/.history/src/handlers/help_handlers_20250830092040.py
deleted file mode 100644
index bcd68f7..0000000
--- a/.history/src/handlers/help_handlers_20250830092040.py
+++ /dev/null
@@ -1,87 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль с функциями для генерации справочных сообщений о командах бота
-"""
-
-import logging
-from telegram import Update
-from telegram.ext import ContextTypes
-from telegram.constants import ParseMode
-
-logger = logging.getLogger(__name__)
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /help - выводит справку по всем доступным командам
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) requested help")
-
- help_text = (
- "🤖 Synology Power Control Bot\n\n"
- "БАЗОВЫЕ КОМАНДЫ:\n"
- "/status - Проверить состояние NAS\n"
- "/power - Управление питанием NAS (меню)\n"
- "/reboot - Перезагрузка NAS (с подтверждением)\n"
- "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
-
- "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n"
- "/system - Информация о системе\n"
- "/storage - Состояние хранилища\n"
- "/shares - Список общих папок\n"
- "/load - Нагрузка на систему\n"
- "/security - Информация о безопасности\n"
- "/temperature - Температура устройства\n"
- "/processes - Список активных процессов\n"
- "/network - Сетевая информация\n\n"
-
- "РАСШИРЕННЫЕ КОМАНДЫ:\n"
- "/schedule - Расписание питания\n"
- "/browse - Просмотр файлов\n"
- "/search <запрос> - Поиск файлов\n"
- "/updates - Проверка обновлений\n"
- "/backup - Статус резервного копирования\n"
- "/quota - Квоты пользователей\n\n"
-
- "БЫСТРЫЕ КОМАНДЫ:\n"
- "/quickreboot - Быстрая перезагрузка\n"
- "/wakeup - Пробуждение NAS (WOL)\n\n"
-
- "СЛУЖЕБНЫЕ КОМАНДЫ:\n"
- "/checkapi - Проверка API\n"
- "/help - Эта справка\n"
- )
-
- if update.message:
- await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
- elif update.callback_query:
- await update.callback_query.answer()
- await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML)
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /start - приветствие и краткая информация
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) started the bot")
-
- welcome_text = (
- "👋 Добро пожаловать в Synology Power Control Bot!\n\n"
- "С помощью этого бота вы можете управлять питанием вашего Synology NAS "
- "и получать различную информацию о его состоянии.\n\n"
- "Для просмотра списка доступных команд используйте /help\n\n"
- "Базовые команды:\n"
- "• /status - Проверить состояние NAS\n"
- "• /power - Управление питанием\n"
- "• /system - Информация о системе\n"
- "• /storage - Состояние хранилища"
- )
-
- if update.message:
- await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)
diff --git a/.history/src/handlers/help_handlers_20250830092051.py b/.history/src/handlers/help_handlers_20250830092051.py
deleted file mode 100644
index 2181fe6..0000000
--- a/.history/src/handlers/help_handlers_20250830092051.py
+++ /dev/null
@@ -1,92 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль с функциями для генерации справочных сообщений о командах бота
-"""
-
-import logging
-from telegram import Update
-from telegram.ext import ContextTypes
-from telegram.constants import ParseMode
-
-logger = logging.getLogger(__name__)
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /help - выводит справку по всем доступным командам
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) requested help")
-
- help_text = (
- "🤖 Synology Power Control Bot\n\n"
- "БАЗОВЫЕ КОМАНДЫ:\n"
- "/status - Проверить состояние NAS\n"
- "/power - Управление питанием NAS (меню)\n"
- "/reboot - Перезагрузка NAS (с подтверждением)\n"
- "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
-
- "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n"
- "/system - Информация о системе\n"
- "/storage - Состояние хранилища\n"
- "/shares - Список общих папок\n"
- "/load - Нагрузка на систему\n"
- "/security - Информация о безопасности\n"
- "/temperature - Температура устройства\n"
- "/processes - Список активных процессов\n"
- "/network - Сетевая информация\n\n"
-
- "РАСШИРЕННЫЕ КОМАНДЫ:\n"
- "/schedule - Расписание питания\n"
- "/browse - Просмотр файлов\n"
- "/search <запрос> - Поиск файлов\n"
- "/updates - Проверка обновлений\n"
- "/backup - Статус резервного копирования\n"
- "/quota - Квоты пользователей\n\n"
-
- "БЫСТРЫЕ КОМАНДЫ:\n"
- "/quickreboot - Быстрая перезагрузка\n"
- "/wakeup - Пробуждение NAS (WOL)\n\n"
-
- "СЛУЖЕБНЫЕ КОМАНДЫ:\n"
- "/checkapi - Проверка API\n"
- "/help - Эта справка\n"
- )
-
- if update.message:
- await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
- elif update.callback_query and update.callback_query.message:
- await update.callback_query.answer()
- try:
- await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML)
- except Exception as e:
- logger.error(f"Failed to edit message: {e}")
- # Отправляем новое сообщение, если не можем отредактировать
- await update.callback_query.message.reply_text(help_text, parse_mode=ParseMode.HTML)
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /start - приветствие и краткая информация
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) started the bot")
-
- welcome_text = (
- "👋 Добро пожаловать в Synology Power Control Bot!\n\n"
- "С помощью этого бота вы можете управлять питанием вашего Synology NAS "
- "и получать различную информацию о его состоянии.\n\n"
- "Для просмотра списка доступных команд используйте /help\n\n"
- "Базовые команды:\n"
- "• /status - Проверить состояние NAS\n"
- "• /power - Управление питанием\n"
- "• /system - Информация о системе\n"
- "• /storage - Состояние хранилища"
- )
-
- if update.message:
- await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)
diff --git a/.history/src/handlers/help_handlers_20250830092139.py b/.history/src/handlers/help_handlers_20250830092139.py
deleted file mode 100644
index 00a9ca6..0000000
--- a/.history/src/handlers/help_handlers_20250830092139.py
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль с функциями для генерации справочных сообщений о командах бота
-"""
-
-import logging
-from telegram import Update
-from telegram.ext import ContextTypes
-from telegram.constants import ParseMode
-
-from src.config.config import ADMIN_USER_IDS
-
-logger = logging.getLogger(__name__)
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /help - выводит справку по всем доступным командам
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) requested help")
-
- # Проверка прав доступа
- if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
- if update.message:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- help_text = (
- "🤖 Synology Power Control Bot\n\n"
- "БАЗОВЫЕ КОМАНДЫ:\n"
- "/status - Проверить состояние NAS\n"
- "/power - Управление питанием NAS (меню)\n"
- "/reboot - Перезагрузка NAS (с подтверждением)\n"
- "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
-
- "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n"
- "/system - Информация о системе\n"
- "/storage - Состояние хранилища\n"
- "/shares - Список общих папок\n"
- "/load - Нагрузка на систему\n"
- "/security - Информация о безопасности\n"
- "/temperature - Температура устройства\n"
- "/processes - Список активных процессов\n"
- "/network - Сетевая информация\n\n"
-
- "РАСШИРЕННЫЕ КОМАНДЫ:\n"
- "/schedule - Расписание питания\n"
- "/browse - Просмотр файлов\n"
- "/search <запрос> - Поиск файлов\n"
- "/updates - Проверка обновлений\n"
- "/backup - Статус резервного копирования\n"
- "/quota - Квоты пользователей\n\n"
-
- "БЫСТРЫЕ КОМАНДЫ:\n"
- "/quickreboot - Быстрая перезагрузка\n"
- "/wakeup - Пробуждение NAS (WOL)\n\n"
-
- "СЛУЖЕБНЫЕ КОМАНДЫ:\n"
- "/checkapi - Проверка API\n"
- "/help - Эта справка\n"
- )
-
- if update.message:
- await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
- elif update.callback_query:
- await update.callback_query.answer()
- if update.callback_query.message:
- try:
- await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML)
- except Exception as e:
- logger.error(f"Failed to edit message: {e}")
- # Отправляем новое сообщение в текущий чат
- await context.bot.send_message(
- chat_id=update.callback_query.message.chat_id,
- text=help_text,
- parse_mode=ParseMode.HTML
- )
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /start - приветствие и краткая информация
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) started the bot")
-
- # Проверка прав доступа
- if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
- if update.message:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- welcome_text = (
- "👋 Добро пожаловать в Synology Power Control Bot!\n\n"
- "С помощью этого бота вы можете управлять питанием вашего Synology NAS "
- "и получать различную информацию о его состоянии.\n\n"
- "Для просмотра списка доступных команд используйте /help\n\n"
- "Базовые команды:\n"
- "• /status - Проверить состояние NAS\n"
- "• /power - Управление питанием\n"
- "• /system - Информация о системе\n"
- "• /storage - Состояние хранилища"
- )
-
- if update.message:
- await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)
diff --git a/.history/src/handlers/help_handlers_20250830092441.py b/.history/src/handlers/help_handlers_20250830092441.py
deleted file mode 100644
index 00a9ca6..0000000
--- a/.history/src/handlers/help_handlers_20250830092441.py
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль с функциями для генерации справочных сообщений о командах бота
-"""
-
-import logging
-from telegram import Update
-from telegram.ext import ContextTypes
-from telegram.constants import ParseMode
-
-from src.config.config import ADMIN_USER_IDS
-
-logger = logging.getLogger(__name__)
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /help - выводит справку по всем доступным командам
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) requested help")
-
- # Проверка прав доступа
- if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
- if update.message:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- help_text = (
- "🤖 Synology Power Control Bot\n\n"
- "БАЗОВЫЕ КОМАНДЫ:\n"
- "/status - Проверить состояние NAS\n"
- "/power - Управление питанием NAS (меню)\n"
- "/reboot - Перезагрузка NAS (с подтверждением)\n"
- "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
-
- "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n"
- "/system - Информация о системе\n"
- "/storage - Состояние хранилища\n"
- "/shares - Список общих папок\n"
- "/load - Нагрузка на систему\n"
- "/security - Информация о безопасности\n"
- "/temperature - Температура устройства\n"
- "/processes - Список активных процессов\n"
- "/network - Сетевая информация\n\n"
-
- "РАСШИРЕННЫЕ КОМАНДЫ:\n"
- "/schedule - Расписание питания\n"
- "/browse - Просмотр файлов\n"
- "/search <запрос> - Поиск файлов\n"
- "/updates - Проверка обновлений\n"
- "/backup - Статус резервного копирования\n"
- "/quota - Квоты пользователей\n\n"
-
- "БЫСТРЫЕ КОМАНДЫ:\n"
- "/quickreboot - Быстрая перезагрузка\n"
- "/wakeup - Пробуждение NAS (WOL)\n\n"
-
- "СЛУЖЕБНЫЕ КОМАНДЫ:\n"
- "/checkapi - Проверка API\n"
- "/help - Эта справка\n"
- )
-
- if update.message:
- await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
- elif update.callback_query:
- await update.callback_query.answer()
- if update.callback_query.message:
- try:
- await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML)
- except Exception as e:
- logger.error(f"Failed to edit message: {e}")
- # Отправляем новое сообщение в текущий чат
- await context.bot.send_message(
- chat_id=update.callback_query.message.chat_id,
- text=help_text,
- parse_mode=ParseMode.HTML
- )
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /start - приветствие и краткая информация
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) started the bot")
-
- # Проверка прав доступа
- if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
- if update.message:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- welcome_text = (
- "👋 Добро пожаловать в Synology Power Control Bot!\n\n"
- "С помощью этого бота вы можете управлять питанием вашего Synology NAS "
- "и получать различную информацию о его состоянии.\n\n"
- "Для просмотра списка доступных команд используйте /help\n\n"
- "Базовые команды:\n"
- "• /status - Проверить состояние NAS\n"
- "• /power - Управление питанием\n"
- "• /system - Информация о системе\n"
- "• /storage - Состояние хранилища"
- )
-
- if update.message:
- await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)
diff --git a/.history/src/handlers/help_handlers_20250830095731.py b/.history/src/handlers/help_handlers_20250830095731.py
deleted file mode 100644
index 80fa480..0000000
--- a/.history/src/handlers/help_handlers_20250830095731.py
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль с функциями для генерации справочных сообщений о командах бота
-"""
-
-import logging
-from telegram import Update
-from telegram.ext import ContextTypes
-from telegram.constants import ParseMode
-
-from src.config.config import ADMIN_USER_IDS
-
-logger = logging.getLogger(__name__)
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /help - выводит справку по всем доступным командам
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) requested help")
-
- # Проверка прав доступа
- if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
- if update.message:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- help_text = (
- "🤖 Synology Power Control Bot\n\n"
- "БАЗОВЫЕ КОМАНДЫ:\n"
- "/status - Проверить состояние NAS\n"
- "/power - Управление питанием NAS (меню)\n"
- "/reboot - Перезагрузка NAS (с подтверждением)\n"
- "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
-
- "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n"
- "/system - Информация о системе\n"
- "/storage - Состояние хранилища\n"
- "/shares - Список общих папок\n"
- "/load - Нагрузка на систему\n"
- "/security - Информация о безопасности\n"
- "/temperature - Температура устройства\n"
- "/processes - Список активных процессов\n"
- "/network - Сетевая информация\n\n"
-
- "РАСШИРЕННЫЕ КОМАНДЫ:\n"
- "/schedule - Расписание питания\n"
- "/browse - Просмотр файлов\n"
- "/search <запрос> - Поиск файлов\n"
- "/updates - Проверка обновлений\n"
- "/backup - Статус резервного копирования\n"
- "/quota - Квоты пользователей\n\n"
-
- "БЫСТРЫЕ КОМАНДЫ:\n"
- "/quickreboot - Быстрая перезагрузка\n"
- "/wakeup - Пробуждение NAS (WOL)\n\n"
-
- "СЛУЖЕБНЫЕ КОМАНДЫ:\n"
- "/checkapi - Проверка API\n"
- "/help - Эта справка\n"
- )
-
- if update.message:
- await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
- elif update.callback_query:
- await update.callback_query.answer()
- if update.callback_query.message:
- try:
- await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML)
- except Exception as e:
- logger.error(f"Failed to edit message: {e}")
- # Отправляем новое сообщение в текущий чат
- await context.bot.send_message(
- chat_id=update.callback_query.message.chat_id,
- text=help_text,
- parse_mode=ParseMode.HTML
- )
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /start - приветствие и краткая информация
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) started the bot")
-
- # Проверка прав доступа
- if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
- if update.message:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- welcome_text = (
- "👋 Добро пожаловать в Synology Power Control Bot!\n\n"
- "С помощью этого бота вы можете управлять питанием вашего Synology NAS "
- "и получать различную информацию о его состоянии.\n\n"
- "Для просмотра списка доступных команд используйте /help\n\n"
- "Базовые команды:\n"
- "• /status - Проверить состояние NAS\n"
- "• /power - Управление питанием\n"
- "• /system - Информация о системе\n"
- "• /storage - Состояние хранилища"
- )
-
- if update.message:
- await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)
diff --git a/.history/src/handlers/help_handlers_20250830095750.py b/.history/src/handlers/help_handlers_20250830095750.py
deleted file mode 100644
index 80fa480..0000000
--- a/.history/src/handlers/help_handlers_20250830095750.py
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль с функциями для генерации справочных сообщений о командах бота
-"""
-
-import logging
-from telegram import Update
-from telegram.ext import ContextTypes
-from telegram.constants import ParseMode
-
-from src.config.config import ADMIN_USER_IDS
-
-logger = logging.getLogger(__name__)
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /help - выводит справку по всем доступным командам
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) requested help")
-
- # Проверка прав доступа
- if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
- if update.message:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- help_text = (
- "🤖 Synology Power Control Bot\n\n"
- "БАЗОВЫЕ КОМАНДЫ:\n"
- "/status - Проверить состояние NAS\n"
- "/power - Управление питанием NAS (меню)\n"
- "/reboot - Перезагрузка NAS (с подтверждением)\n"
- "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
-
- "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n"
- "/system - Информация о системе\n"
- "/storage - Состояние хранилища\n"
- "/shares - Список общих папок\n"
- "/load - Нагрузка на систему\n"
- "/security - Информация о безопасности\n"
- "/temperature - Температура устройства\n"
- "/processes - Список активных процессов\n"
- "/network - Сетевая информация\n\n"
-
- "РАСШИРЕННЫЕ КОМАНДЫ:\n"
- "/schedule - Расписание питания\n"
- "/browse - Просмотр файлов\n"
- "/search <запрос> - Поиск файлов\n"
- "/updates - Проверка обновлений\n"
- "/backup - Статус резервного копирования\n"
- "/quota - Квоты пользователей\n\n"
-
- "БЫСТРЫЕ КОМАНДЫ:\n"
- "/quickreboot - Быстрая перезагрузка\n"
- "/wakeup - Пробуждение NAS (WOL)\n\n"
-
- "СЛУЖЕБНЫЕ КОМАНДЫ:\n"
- "/checkapi - Проверка API\n"
- "/help - Эта справка\n"
- )
-
- if update.message:
- await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
- elif update.callback_query:
- await update.callback_query.answer()
- if update.callback_query.message:
- try:
- await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML)
- except Exception as e:
- logger.error(f"Failed to edit message: {e}")
- # Отправляем новое сообщение в текущий чат
- await context.bot.send_message(
- chat_id=update.callback_query.message.chat_id,
- text=help_text,
- parse_mode=ParseMode.HTML
- )
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /start - приветствие и краткая информация
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) started the bot")
-
- # Проверка прав доступа
- if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
- if update.message:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- welcome_text = (
- "👋 Добро пожаловать в Synology Power Control Bot!\n\n"
- "С помощью этого бота вы можете управлять питанием вашего Synology NAS "
- "и получать различную информацию о его состоянии.\n\n"
- "Для просмотра списка доступных команд используйте /help\n\n"
- "Базовые команды:\n"
- "• /status - Проверить состояние NAS\n"
- "• /power - Управление питанием\n"
- "• /system - Информация о системе\n"
- "• /storage - Состояние хранилища"
- )
-
- if update.message:
- await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)
diff --git a/.history/src/handlers/help_handlers_20250830110705.py b/.history/src/handlers/help_handlers_20250830110705.py
deleted file mode 100644
index 702186c..0000000
--- a/.history/src/handlers/help_handlers_20250830110705.py
+++ /dev/null
@@ -1,116 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль с функциями для генерации справочных сообщений о командах бота
-"""
-
-import logging
-from telegram import Update
-from telegram.ext import ContextTypes
-from telegram.constants import ParseMode
-
-from src.config.config import ADMIN_USER_IDS
-
-logger = logging.getLogger(__name__)
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /help - выводит справку по всем доступным командам
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) requested help")
-
- # Проверка прав доступа
- if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
- if update.message:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- help_text = (
- "🤖 Synology Power Control Bot\n\n"
- "БАЗОВЫЕ КОМАНДЫ:\n"
- "/status - Проверить состояние NAS\n"
- "/power - Управление питанием NAS (меню)\n"
- "/reboot - Перезагрузка NAS (с подтверждением)\n"
- "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
-
- "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n"
- "/system - Информация о системе\n"
- "/storage - Состояние хранилища\n"
- "/shares - Список общих папок\n"
- "/load - Нагрузка на систему\n"
- "/security - Информация о безопасности\n"
- "/temperature - Температура устройства\n"
- "/processes - Список активных процессов\n"
- "/network - Сетевая информация\n\n"
-
- "РАСШИРЕННЫЕ КОМАНДЫ:\n"
- "/schedule - Расписание питания\n"
- "/browse - Просмотр файлов\n"
- "/search <запрос> - Поиск файлов\n"
- "/updates - Проверка обновлений\n"
- "/backup - Статус резервного копирования\n"
- "/quota - Квоты пользователей\n\n"
-
- "БЫСТРЫЕ КОМАНДЫ:\n"
- "/quickreboot - Быстрая перезагрузка\n"
- "/wakeup - Пробуждение NAS (WOL)\n\n"
-
- "СЛУЖЕБНЫЕ КОМАНДЫ:\n"
- "/checkapi - Проверка API\n"
- "/help - Эта справка\n\n"
-
- "УПРАВЛЕНИЕ АДМИНИСТРАТОРАМИ:\n"
- "/admins - Список администраторов\n"
- "/addadmin <id> - Добавить администратора\n"
- "/removeadmin <id> - Удалить администратора\n"
- )
-
- if update.message:
- await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
- elif update.callback_query:
- await update.callback_query.answer()
- if update.callback_query.message:
- try:
- await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML)
- except Exception as e:
- logger.error(f"Failed to edit message: {e}")
- # Отправляем новое сообщение в текущий чат
- await context.bot.send_message(
- chat_id=update.callback_query.message.chat_id,
- text=help_text,
- parse_mode=ParseMode.HTML
- )
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /start - приветствие и краткая информация
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) started the bot")
-
- # Проверка прав доступа
- if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
- if update.message:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- welcome_text = (
- "👋 Добро пожаловать в Synology Power Control Bot!\n\n"
- "С помощью этого бота вы можете управлять питанием вашего Synology NAS "
- "и получать различную информацию о его состоянии.\n\n"
- "Для просмотра списка доступных команд используйте /help\n\n"
- "Базовые команды:\n"
- "• /status - Проверить состояние NAS\n"
- "• /power - Управление питанием\n"
- "• /system - Информация о системе\n"
- "• /storage - Состояние хранилища"
- )
-
- if update.message:
- await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)
diff --git a/.history/src/handlers/help_handlers_20250830110906.py b/.history/src/handlers/help_handlers_20250830110906.py
deleted file mode 100644
index 702186c..0000000
--- a/.history/src/handlers/help_handlers_20250830110906.py
+++ /dev/null
@@ -1,116 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль с функциями для генерации справочных сообщений о командах бота
-"""
-
-import logging
-from telegram import Update
-from telegram.ext import ContextTypes
-from telegram.constants import ParseMode
-
-from src.config.config import ADMIN_USER_IDS
-
-logger = logging.getLogger(__name__)
-
-async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /help - выводит справку по всем доступным командам
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) requested help")
-
- # Проверка прав доступа
- if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
- if update.message:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- help_text = (
- "🤖 Synology Power Control Bot\n\n"
- "БАЗОВЫЕ КОМАНДЫ:\n"
- "/status - Проверить состояние NAS\n"
- "/power - Управление питанием NAS (меню)\n"
- "/reboot - Перезагрузка NAS (с подтверждением)\n"
- "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n"
-
- "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n"
- "/system - Информация о системе\n"
- "/storage - Состояние хранилища\n"
- "/shares - Список общих папок\n"
- "/load - Нагрузка на систему\n"
- "/security - Информация о безопасности\n"
- "/temperature - Температура устройства\n"
- "/processes - Список активных процессов\n"
- "/network - Сетевая информация\n\n"
-
- "РАСШИРЕННЫЕ КОМАНДЫ:\n"
- "/schedule - Расписание питания\n"
- "/browse - Просмотр файлов\n"
- "/search <запрос> - Поиск файлов\n"
- "/updates - Проверка обновлений\n"
- "/backup - Статус резервного копирования\n"
- "/quota - Квоты пользователей\n\n"
-
- "БЫСТРЫЕ КОМАНДЫ:\n"
- "/quickreboot - Быстрая перезагрузка\n"
- "/wakeup - Пробуждение NAS (WOL)\n\n"
-
- "СЛУЖЕБНЫЕ КОМАНДЫ:\n"
- "/checkapi - Проверка API\n"
- "/help - Эта справка\n\n"
-
- "УПРАВЛЕНИЕ АДМИНИСТРАТОРАМИ:\n"
- "/admins - Список администраторов\n"
- "/addadmin <id> - Добавить администратора\n"
- "/removeadmin <id> - Удалить администратора\n"
- )
-
- if update.message:
- await update.message.reply_text(help_text, parse_mode=ParseMode.HTML)
- elif update.callback_query:
- await update.callback_query.answer()
- if update.callback_query.message:
- try:
- await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML)
- except Exception as e:
- logger.error(f"Failed to edit message: {e}")
- # Отправляем новое сообщение в текущий чат
- await context.bot.send_message(
- chat_id=update.callback_query.message.chat_id,
- text=help_text,
- parse_mode=ParseMode.HTML
- )
-
-async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """
- Обработчик команды /start - приветствие и краткая информация
- """
- user_id = update.effective_user.id if update.effective_user else "Unknown"
- username = update.effective_user.username if update.effective_user else "Unknown"
-
- logger.info(f"User {user_id} (@{username}) started the bot")
-
- # Проверка прав доступа
- if user_id not in ADMIN_USER_IDS and isinstance(user_id, int):
- if update.message:
- await update.message.reply_text("У вас нет доступа к этому боту.")
- return
-
- welcome_text = (
- "👋 Добро пожаловать в Synology Power Control Bot!\n\n"
- "С помощью этого бота вы можете управлять питанием вашего Synology NAS "
- "и получать различную информацию о его состоянии.\n\n"
- "Для просмотра списка доступных команд используйте /help\n\n"
- "Базовые команды:\n"
- "• /status - Проверить состояние NAS\n"
- "• /power - Управление питанием\n"
- "• /system - Информация о системе\n"
- "• /storage - Состояние хранилища"
- )
-
- if update.message:
- await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML)
diff --git a/.history/src/healthcheck_20250830102839.py b/.history/src/healthcheck_20250830102839.py
deleted file mode 100644
index 0ec9b8e..0000000
--- a/.history/src/healthcheck_20250830102839.py
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Простой HTTP-сервер для healthcheck Docker-контейнера.
-Запускается параллельно с основным ботом и отвечает на запросы /health.
-"""
-
-import os
-import http.server
-import socketserver
-import threading
-import logging
-from time import sleep
-
-# Настройка логирования
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger('healthcheck')
-
-# Порт для healthcheck
-PORT = int(os.getenv('HEALTHCHECK_PORT', 8080))
-
-class HealthCheckHandler(http.server.SimpleHTTPRequestHandler):
- """Обработчик для healthcheck запросов"""
-
- def do_GET(self):
- """Обработка GET-запросов"""
- if self.path == '/health':
- self.send_response(200)
- self.send_header('Content-Type', 'text/plain')
- self.end_headers()
- self.wfile.write(b"OK")
- else:
- self.send_response(404)
- self.send_header('Content-Type', 'text/plain')
- self.end_headers()
- self.wfile.write(b"Not Found")
-
- def log_message(self, format, *args):
- """Переопределяем метод логирования для вывода в наш logger"""
- logger.info("%s - - [%s] %s", self.address_string(), self.log_date_time_string(), format % args)
-
-def run_health_server():
- """Запуск HTTP-сервера для healthcheck"""
- with socketserver.TCPServer(("", PORT), HealthCheckHandler) as httpd:
- logger.info(f"Starting healthcheck server on port {PORT}")
- httpd.serve_forever()
-
-def start_health_server():
- """Запуск сервера в отдельном потоке"""
- # Даем основному приложению время на инициализацию
- sleep(5)
-
- # Запускаем HTTP-сервер в отдельном потоке
- thread = threading.Thread(target=run_health_server, daemon=True)
- thread.start()
- logger.info("Healthcheck server thread started")
- return thread
-
-if __name__ == "__main__":
- # Этот код выполняется только если файл запускается напрямую, а не импортируется
- thread = start_health_server()
- try:
- # Держим основной поток живым
- while True:
- sleep(60)
- except KeyboardInterrupt:
- logger.info("Healthcheck server shutting down")
diff --git a/.history/src/healthcheck_20250830103154.py b/.history/src/healthcheck_20250830103154.py
deleted file mode 100644
index 0ec9b8e..0000000
--- a/.history/src/healthcheck_20250830103154.py
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Простой HTTP-сервер для healthcheck Docker-контейнера.
-Запускается параллельно с основным ботом и отвечает на запросы /health.
-"""
-
-import os
-import http.server
-import socketserver
-import threading
-import logging
-from time import sleep
-
-# Настройка логирования
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger('healthcheck')
-
-# Порт для healthcheck
-PORT = int(os.getenv('HEALTHCHECK_PORT', 8080))
-
-class HealthCheckHandler(http.server.SimpleHTTPRequestHandler):
- """Обработчик для healthcheck запросов"""
-
- def do_GET(self):
- """Обработка GET-запросов"""
- if self.path == '/health':
- self.send_response(200)
- self.send_header('Content-Type', 'text/plain')
- self.end_headers()
- self.wfile.write(b"OK")
- else:
- self.send_response(404)
- self.send_header('Content-Type', 'text/plain')
- self.end_headers()
- self.wfile.write(b"Not Found")
-
- def log_message(self, format, *args):
- """Переопределяем метод логирования для вывода в наш logger"""
- logger.info("%s - - [%s] %s", self.address_string(), self.log_date_time_string(), format % args)
-
-def run_health_server():
- """Запуск HTTP-сервера для healthcheck"""
- with socketserver.TCPServer(("", PORT), HealthCheckHandler) as httpd:
- logger.info(f"Starting healthcheck server on port {PORT}")
- httpd.serve_forever()
-
-def start_health_server():
- """Запуск сервера в отдельном потоке"""
- # Даем основному приложению время на инициализацию
- sleep(5)
-
- # Запускаем HTTP-сервер в отдельном потоке
- thread = threading.Thread(target=run_health_server, daemon=True)
- thread.start()
- logger.info("Healthcheck server thread started")
- return thread
-
-if __name__ == "__main__":
- # Этот код выполняется только если файл запускается напрямую, а не импортируется
- thread = start_health_server()
- try:
- # Держим основной поток живым
- while True:
- sleep(60)
- except KeyboardInterrupt:
- logger.info("Healthcheck server shutting down")
diff --git a/.history/src/utils/admin_utils_20250830110540.py b/.history/src/utils/admin_utils_20250830110540.py
deleted file mode 100644
index f85b013..0000000
--- a/.history/src/utils/admin_utils_20250830110540.py
+++ /dev/null
@@ -1,283 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Утилиты для управления администраторами бота
-"""
-
-import os
-import logging
-from typing import List, Optional, Callable, Any, Union
-from functools import wraps
-from telegram import Update
-from telegram.ext import ContextTypes
-from src.config.config import ADMIN_USER_IDS
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-def is_admin(user_id: int) -> bool:
- """Проверяет, является ли пользователь администратором бота
-
- Args:
- user_id: ID пользователя Telegram
-
- Returns:
- True если пользователь администратор, иначе False
- """
- return user_id in ADMIN_USER_IDS
-
-def admin_required(func: Callable) -> Callable:
- """Декоратор для проверки, является ли пользователь администратором
-
- Args:
- func: Оригинальная функция обработчика
-
- Returns:
- Обернутая функция с проверкой прав администратора
- """
- @wraps(func)
- async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args: Any, **kwargs: Any) -> Any:
- # Проверяем доступность объекта update и effective_user
- if not update or not update.effective_user:
- logger.warning("Update object is incomplete, unable to check admin status")
- return
-
- user_id = update.effective_user.id
- username = update.effective_user.username or "Unknown"
-
- if not is_admin(user_id):
- logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
-
- # Если это сообщение, отправляем уведомление
- if update.message:
- await update.message.reply_text(
- "⛔️ У вас нет прав на использование этой команды.\n"
- "Обратитесь к владельцу бота, чтобы получить доступ."
- )
- # Если это callback query, отвечаем на него
- elif update.callback_query:
- await update.callback_query.answer(
- "⛔️ У вас нет прав на использование этой функции."
- )
- return
-
- # Если пользователь админ, вызываем оригинальную функцию
- return await func(update, context, *args, **kwargs)
-
- return wrapper
-
-async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Добавляет нового администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID нового администратора
- new_admin_id = int(context.args[0])
-
- # Проверяем, не является ли пользователь уже администратором
- if new_admin_id in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {new_admin_id} уже является администратором.")
- return
-
- # Добавляем нового администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Обновляем или добавляем строку с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip()
- new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
- lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
- else:
- lines.append(f"ADMIN_USER_IDS={new_admin_id}")
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.append(new_admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {new_admin_id} добавлен в список администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error adding admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при добавлении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Удаляет администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID администратора для удаления
- admin_id = int(context.args[0])
-
- # Проверяем, не удаляет ли админ сам себя
- if admin_id == update.effective_user.id:
- await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
- return
-
- # Проверяем, является ли пользователь администратором
- if admin_id not in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {admin_id} не является администратором.")
- return
-
- # Удаляем администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Удаляем ID из строки с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
- new_ids = [id for id in current_ids if int(id) != admin_id]
-
- if not new_ids:
- # Если не осталось администраторов, добавляем текущего пользователя
- # чтобы избежать ситуации, когда нет администраторов
- new_ids = [str(update.effective_user.id)]
-
- lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.remove(admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {admin_id} удален из списка администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
- else:
- await update.message.reply_text("❌ Ошибка: Не удалось найти настройки администраторов.")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error removing admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при удалении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Показывает список администраторов бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- try:
- if not ADMIN_USER_IDS:
- await update.message.reply_text("⚠️ Список администраторов пуст.")
- return
-
- # Формируем сообщение со списком администраторов
- admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_USER_IDS])
-
- await update.message.reply_text(
- f"👑 Список администраторов бота:\n\n"
- f"{admin_list}\n\n"
- f"Всего администраторов: {len(ADMIN_USER_IDS)}",
- parse_mode="HTML"
- )
-
- except Exception as e:
- logger.error(f"Error listing admins: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при получении списка администраторов:\n\n{str(e)}",
- parse_mode="HTML"
- )
diff --git a/.history/src/utils/admin_utils_20250830110906.py b/.history/src/utils/admin_utils_20250830110906.py
deleted file mode 100644
index f85b013..0000000
--- a/.history/src/utils/admin_utils_20250830110906.py
+++ /dev/null
@@ -1,283 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Утилиты для управления администраторами бота
-"""
-
-import os
-import logging
-from typing import List, Optional, Callable, Any, Union
-from functools import wraps
-from telegram import Update
-from telegram.ext import ContextTypes
-from src.config.config import ADMIN_USER_IDS
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-def is_admin(user_id: int) -> bool:
- """Проверяет, является ли пользователь администратором бота
-
- Args:
- user_id: ID пользователя Telegram
-
- Returns:
- True если пользователь администратор, иначе False
- """
- return user_id in ADMIN_USER_IDS
-
-def admin_required(func: Callable) -> Callable:
- """Декоратор для проверки, является ли пользователь администратором
-
- Args:
- func: Оригинальная функция обработчика
-
- Returns:
- Обернутая функция с проверкой прав администратора
- """
- @wraps(func)
- async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args: Any, **kwargs: Any) -> Any:
- # Проверяем доступность объекта update и effective_user
- if not update or not update.effective_user:
- logger.warning("Update object is incomplete, unable to check admin status")
- return
-
- user_id = update.effective_user.id
- username = update.effective_user.username or "Unknown"
-
- if not is_admin(user_id):
- logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
-
- # Если это сообщение, отправляем уведомление
- if update.message:
- await update.message.reply_text(
- "⛔️ У вас нет прав на использование этой команды.\n"
- "Обратитесь к владельцу бота, чтобы получить доступ."
- )
- # Если это callback query, отвечаем на него
- elif update.callback_query:
- await update.callback_query.answer(
- "⛔️ У вас нет прав на использование этой функции."
- )
- return
-
- # Если пользователь админ, вызываем оригинальную функцию
- return await func(update, context, *args, **kwargs)
-
- return wrapper
-
-async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Добавляет нового администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID нового администратора
- new_admin_id = int(context.args[0])
-
- # Проверяем, не является ли пользователь уже администратором
- if new_admin_id in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {new_admin_id} уже является администратором.")
- return
-
- # Добавляем нового администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Обновляем или добавляем строку с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip()
- new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
- lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
- else:
- lines.append(f"ADMIN_USER_IDS={new_admin_id}")
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.append(new_admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {new_admin_id} добавлен в список администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error adding admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при добавлении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Удаляет администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID администратора для удаления
- admin_id = int(context.args[0])
-
- # Проверяем, не удаляет ли админ сам себя
- if admin_id == update.effective_user.id:
- await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
- return
-
- # Проверяем, является ли пользователь администратором
- if admin_id not in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {admin_id} не является администратором.")
- return
-
- # Удаляем администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Удаляем ID из строки с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
- new_ids = [id for id in current_ids if int(id) != admin_id]
-
- if not new_ids:
- # Если не осталось администраторов, добавляем текущего пользователя
- # чтобы избежать ситуации, когда нет администраторов
- new_ids = [str(update.effective_user.id)]
-
- lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.remove(admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {admin_id} удален из списка администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
- else:
- await update.message.reply_text("❌ Ошибка: Не удалось найти настройки администраторов.")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error removing admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при удалении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Показывает список администраторов бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- try:
- if not ADMIN_USER_IDS:
- await update.message.reply_text("⚠️ Список администраторов пуст.")
- return
-
- # Формируем сообщение со списком администраторов
- admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_USER_IDS])
-
- await update.message.reply_text(
- f"👑 Список администраторов бота:\n\n"
- f"{admin_list}\n\n"
- f"Всего администраторов: {len(ADMIN_USER_IDS)}",
- parse_mode="HTML"
- )
-
- except Exception as e:
- logger.error(f"Error listing admins: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при получении списка администраторов:\n\n{str(e)}",
- parse_mode="HTML"
- )
diff --git a/.history/src/utils/admin_utils_20250830114406.py b/.history/src/utils/admin_utils_20250830114406.py
deleted file mode 100644
index f226808..0000000
--- a/.history/src/utils/admin_utils_20250830114406.py
+++ /dev/null
@@ -1,302 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Утилиты для управления администраторами бота
-"""
-
-import os
-import logging
-from typing import List, Optional, Callable, Any, Union
-from functools import wraps
-from telegram import Update
-from telegram.ext import ContextTypes
-from src.config.config import ADMIN_USER_IDS
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-def is_admin(user_id: int) -> bool:
- """Проверяет, является ли пользователь администратором бота
-
- Args:
- user_id: ID пользователя Telegram
-
- Returns:
- True если пользователь администратор, иначе False
- """
- # Получаем актуальный список администраторов из .env файла
- try:
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- if os.path.exists(env_path):
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Ищем строку с ADMIN_USER_IDS
- for line in env_content.split('\n'):
- if line.startswith('ADMIN_USER_IDS='):
- admin_ids_str = line.split('=')[1].strip()
- if admin_ids_str:
- admin_ids = list(map(int, admin_ids_str.split(',')))
- return user_id in admin_ids
- except Exception as e:
- logger.error(f"Error reading admin IDs from .env: {e}")
-
- # Если не удалось прочитать из файла, используем загруженные при старте
- return user_id in ADMIN_USER_IDS
-
-def admin_required(func: Callable) -> Callable:
- """Декоратор для проверки, является ли пользователь администратором
-
- Args:
- func: Оригинальная функция обработчика
-
- Returns:
- Обернутая функция с проверкой прав администратора
- """
- @wraps(func)
- async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args: Any, **kwargs: Any) -> Any:
- # Проверяем доступность объекта update и effective_user
- if not update or not update.effective_user:
- logger.warning("Update object is incomplete, unable to check admin status")
- return
-
- user_id = update.effective_user.id
- username = update.effective_user.username or "Unknown"
-
- if not is_admin(user_id):
- logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
-
- # Если это сообщение, отправляем уведомление
- if update.message:
- await update.message.reply_text(
- "⛔️ У вас нет прав на использование этой команды.\n"
- "Обратитесь к владельцу бота, чтобы получить доступ."
- )
- # Если это callback query, отвечаем на него
- elif update.callback_query:
- await update.callback_query.answer(
- "⛔️ У вас нет прав на использование этой функции."
- )
- return
-
- # Если пользователь админ, вызываем оригинальную функцию
- return await func(update, context, *args, **kwargs)
-
- return wrapper
-
-async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Добавляет нового администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID нового администратора
- new_admin_id = int(context.args[0])
-
- # Проверяем, не является ли пользователь уже администратором
- if new_admin_id in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {new_admin_id} уже является администратором.")
- return
-
- # Добавляем нового администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Обновляем или добавляем строку с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip()
- new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
- lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
- else:
- lines.append(f"ADMIN_USER_IDS={new_admin_id}")
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.append(new_admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {new_admin_id} добавлен в список администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error adding admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при добавлении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Удаляет администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID администратора для удаления
- admin_id = int(context.args[0])
-
- # Проверяем, не удаляет ли админ сам себя
- if admin_id == update.effective_user.id:
- await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
- return
-
- # Проверяем, является ли пользователь администратором
- if admin_id not in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {admin_id} не является администратором.")
- return
-
- # Удаляем администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Удаляем ID из строки с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
- new_ids = [id for id in current_ids if int(id) != admin_id]
-
- if not new_ids:
- # Если не осталось администраторов, добавляем текущего пользователя
- # чтобы избежать ситуации, когда нет администраторов
- new_ids = [str(update.effective_user.id)]
-
- lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.remove(admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {admin_id} удален из списка администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
- else:
- await update.message.reply_text("❌ Ошибка: Не удалось найти настройки администраторов.")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error removing admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при удалении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Показывает список администраторов бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- try:
- if not ADMIN_USER_IDS:
- await update.message.reply_text("⚠️ Список администраторов пуст.")
- return
-
- # Формируем сообщение со списком администраторов
- admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_USER_IDS])
-
- await update.message.reply_text(
- f"👑 Список администраторов бота:\n\n"
- f"{admin_list}\n\n"
- f"Всего администраторов: {len(ADMIN_USER_IDS)}",
- parse_mode="HTML"
- )
-
- except Exception as e:
- logger.error(f"Error listing admins: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при получении списка администраторов:\n\n{str(e)}",
- parse_mode="HTML"
- )
diff --git a/.history/src/utils/admin_utils_20250830114514.py b/.history/src/utils/admin_utils_20250830114514.py
deleted file mode 100644
index f85b013..0000000
--- a/.history/src/utils/admin_utils_20250830114514.py
+++ /dev/null
@@ -1,283 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Утилиты для управления администраторами бота
-"""
-
-import os
-import logging
-from typing import List, Optional, Callable, Any, Union
-from functools import wraps
-from telegram import Update
-from telegram.ext import ContextTypes
-from src.config.config import ADMIN_USER_IDS
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-def is_admin(user_id: int) -> bool:
- """Проверяет, является ли пользователь администратором бота
-
- Args:
- user_id: ID пользователя Telegram
-
- Returns:
- True если пользователь администратор, иначе False
- """
- return user_id in ADMIN_USER_IDS
-
-def admin_required(func: Callable) -> Callable:
- """Декоратор для проверки, является ли пользователь администратором
-
- Args:
- func: Оригинальная функция обработчика
-
- Returns:
- Обернутая функция с проверкой прав администратора
- """
- @wraps(func)
- async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args: Any, **kwargs: Any) -> Any:
- # Проверяем доступность объекта update и effective_user
- if not update or not update.effective_user:
- logger.warning("Update object is incomplete, unable to check admin status")
- return
-
- user_id = update.effective_user.id
- username = update.effective_user.username or "Unknown"
-
- if not is_admin(user_id):
- logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
-
- # Если это сообщение, отправляем уведомление
- if update.message:
- await update.message.reply_text(
- "⛔️ У вас нет прав на использование этой команды.\n"
- "Обратитесь к владельцу бота, чтобы получить доступ."
- )
- # Если это callback query, отвечаем на него
- elif update.callback_query:
- await update.callback_query.answer(
- "⛔️ У вас нет прав на использование этой функции."
- )
- return
-
- # Если пользователь админ, вызываем оригинальную функцию
- return await func(update, context, *args, **kwargs)
-
- return wrapper
-
-async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Добавляет нового администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID нового администратора
- new_admin_id = int(context.args[0])
-
- # Проверяем, не является ли пользователь уже администратором
- if new_admin_id in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {new_admin_id} уже является администратором.")
- return
-
- # Добавляем нового администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Обновляем или добавляем строку с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip()
- new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
- lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
- else:
- lines.append(f"ADMIN_USER_IDS={new_admin_id}")
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.append(new_admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {new_admin_id} добавлен в список администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error adding admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при добавлении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Удаляет администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID администратора для удаления
- admin_id = int(context.args[0])
-
- # Проверяем, не удаляет ли админ сам себя
- if admin_id == update.effective_user.id:
- await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
- return
-
- # Проверяем, является ли пользователь администратором
- if admin_id not in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {admin_id} не является администратором.")
- return
-
- # Удаляем администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Удаляем ID из строки с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
- new_ids = [id for id in current_ids if int(id) != admin_id]
-
- if not new_ids:
- # Если не осталось администраторов, добавляем текущего пользователя
- # чтобы избежать ситуации, когда нет администраторов
- new_ids = [str(update.effective_user.id)]
-
- lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.remove(admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {admin_id} удален из списка администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
- else:
- await update.message.reply_text("❌ Ошибка: Не удалось найти настройки администраторов.")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error removing admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при удалении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Показывает список администраторов бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- try:
- if not ADMIN_USER_IDS:
- await update.message.reply_text("⚠️ Список администраторов пуст.")
- return
-
- # Формируем сообщение со списком администраторов
- admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_USER_IDS])
-
- await update.message.reply_text(
- f"👑 Список администраторов бота:\n\n"
- f"{admin_list}\n\n"
- f"Всего администраторов: {len(ADMIN_USER_IDS)}",
- parse_mode="HTML"
- )
-
- except Exception as e:
- logger.error(f"Error listing admins: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при получении списка администраторов:\n\n{str(e)}",
- parse_mode="HTML"
- )
diff --git a/.history/src/utils/admin_utils_20250830142344.py b/.history/src/utils/admin_utils_20250830142344.py
deleted file mode 100644
index 582e2b0..0000000
--- a/.history/src/utils/admin_utils_20250830142344.py
+++ /dev/null
@@ -1,295 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Утилиты для управления администраторами бота
-"""
-
-import os
-import logging
-from typing import List, Optional, Callable, Any, Union
-from functools import wraps
-from telegram import Update
-from telegram.ext import ContextTypes
-from src.config.config import ADMIN_USER_IDS
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-def is_admin(user_id: int) -> bool:
- """Проверяет, является ли пользователь администратором бота
-
- Args:
- user_id: ID пользователя Telegram
-
- Returns:
- True если пользователь администратор, иначе False
- """
- return user_id in ADMIN_USER_IDS
-
-def admin_required(func: Callable) -> Callable:
- """Декоратор для проверки, является ли пользователь администратором
-
- Args:
- func: Оригинальная функция обработчика
-
- Returns:
- Обернутая функция с проверкой прав администратора
- """
- @wraps(func)
- async def wrapper(*args: Any, **kwargs: Any) -> Any:
- # Определяем, является ли функция методом класса
- # Если первый аргумент - это self, то второй должен быть update
- if len(args) >= 2 and isinstance(args[1], Update):
- self_obj = args[0]
- update = args[1]
- context = args[2] if len(args) > 2 else kwargs.get('context')
- else:
- # Если это обычная функция, то первый аргумент - update
- update = args[0] if args else kwargs.get('update')
- context = args[1] if len(args) > 1 else kwargs.get('context')
- self_obj = None
-
- # Проверяем доступность объекта update и effective_user
- if not update or not update.effective_user:
- logger.warning("Update object is incomplete, unable to check admin status")
- return
-
- user_id = update.effective_user.id
- username = update.effective_user.username or "Unknown"
-
- if not is_admin(user_id):
- logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
-
- # Если это сообщение, отправляем уведомление
- if update.message:
- await update.message.reply_text(
- "⛔️ У вас нет прав на использование этой команды.\n"
- "Обратитесь к владельцу бота, чтобы получить доступ."
- )
- # Если это callback query, отвечаем на него
- elif update.callback_query:
- await update.callback_query.answer(
- "⛔️ У вас нет прав на использование этой функции."
- )
- return
-
- # Если пользователь админ, вызываем оригинальную функцию
- return await func(*args, **kwargs)
-
- return wrapper
-
-async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Добавляет нового администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID нового администратора
- new_admin_id = int(context.args[0])
-
- # Проверяем, не является ли пользователь уже администратором
- if new_admin_id in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {new_admin_id} уже является администратором.")
- return
-
- # Добавляем нового администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Обновляем или добавляем строку с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip()
- new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
- lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
- else:
- lines.append(f"ADMIN_USER_IDS={new_admin_id}")
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.append(new_admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {new_admin_id} добавлен в список администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error adding admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при добавлении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Удаляет администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID администратора для удаления
- admin_id = int(context.args[0])
-
- # Проверяем, не удаляет ли админ сам себя
- if admin_id == update.effective_user.id:
- await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
- return
-
- # Проверяем, является ли пользователь администратором
- if admin_id not in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {admin_id} не является администратором.")
- return
-
- # Удаляем администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Удаляем ID из строки с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
- new_ids = [id for id in current_ids if int(id) != admin_id]
-
- if not new_ids:
- # Если не осталось администраторов, добавляем текущего пользователя
- # чтобы избежать ситуации, когда нет администраторов
- new_ids = [str(update.effective_user.id)]
-
- lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.remove(admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {admin_id} удален из списка администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
- else:
- await update.message.reply_text("❌ Ошибка: Не удалось найти настройки администраторов.")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error removing admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при удалении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Показывает список администраторов бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- try:
- if not ADMIN_USER_IDS:
- await update.message.reply_text("⚠️ Список администраторов пуст.")
- return
-
- # Формируем сообщение со списком администраторов
- admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_USER_IDS])
-
- await update.message.reply_text(
- f"👑 Список администраторов бота:\n\n"
- f"{admin_list}\n\n"
- f"Всего администраторов: {len(ADMIN_USER_IDS)}",
- parse_mode="HTML"
- )
-
- except Exception as e:
- logger.error(f"Error listing admins: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при получении списка администраторов:\n\n{str(e)}",
- parse_mode="HTML"
- )
diff --git a/.history/src/utils/admin_utils_20250830142408.py b/.history/src/utils/admin_utils_20250830142408.py
deleted file mode 100644
index 88c03d4..0000000
--- a/.history/src/utils/admin_utils_20250830142408.py
+++ /dev/null
@@ -1,298 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Утилиты для управления администраторами бота
-"""
-
-import os
-import logging
-from typing import List, Optional, Callable, Any, Union
-from functools import wraps
-from telegram import Update
-from telegram.ext import ContextTypes
-from src.config.config import ADMIN_USER_IDS
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-def is_admin(user_id: int) -> bool:
- """Проверяет, является ли пользователь администратором бота
-
- Args:
- user_id: ID пользователя Telegram
-
- Returns:
- True если пользователь администратор, иначе False
- """
- return user_id in ADMIN_USER_IDS
-
-def admin_required(func: Callable) -> Callable:
- """Декоратор для проверки, является ли пользователь администратором
-
- Args:
- func: Оригинальная функция обработчика
-
- Returns:
- Обернутая функция с проверкой прав администратора
- """
- @wraps(func)
- async def wrapper(*args: Any, **kwargs: Any) -> Any:
- # Определяем, является ли функция методом класса
- # Если первый аргумент - это self, то второй должен быть update
- if len(args) >= 2 and isinstance(args[1], Update):
- self_obj = args[0]
- update = args[1]
- context = args[2] if len(args) > 2 else kwargs.get('context')
- else:
- # Если это обычная функция, то первый аргумент - update
- update = args[0] if args else kwargs.get('update')
- context = args[1] if len(args) > 1 else kwargs.get('context')
- self_obj = None
-
- # Проверяем доступность объекта update и effective_user
- if not update or not update.effective_user:
- logger.warning("Update object is incomplete, unable to check admin status")
- return
-
- user_id = update.effective_user.id
- username = update.effective_user.username or "Unknown"
-
- if not is_admin(user_id):
- logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
-
- # Если это сообщение, отправляем уведомление
- if update.message:
- await update.message.reply_text(
- "⛔️ У вас нет прав на использование этой команды.\n"
- "Обратитесь к владельцу бота, чтобы получить доступ."
- )
- # Если это callback query, отвечаем на него
- elif update.callback_query:
- await update.callback_query.answer(
- "⛔️ У вас нет прав на использование этой функции."
- )
- return
-
- # Если пользователь админ, вызываем оригинальную функцию
- return await func(*args, **kwargs)
-
- return wrapper
-
-async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Добавляет нового администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID нового администратора
- new_admin_id = int(context.args[0])
-
- # Проверяем, не является ли пользователь уже администратором
- if new_admin_id in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {new_admin_id} уже является администратором.")
- return
-
- # Добавляем нового администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Обновляем или добавляем строку с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip()
- new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
- lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
- else:
- lines.append(f"ADMIN_USER_IDS={new_admin_id}")
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.append(new_admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {new_admin_id} добавлен в список администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error adding admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при добавлении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Удаляет администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID администратора для удаления
- admin_id = int(context.args[0])
-
- # Проверяем, не удаляет ли админ сам себя
- if admin_id == update.effective_user.id:
- await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
- return
-
- # Проверяем, является ли пользователь администратором
- if admin_id not in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {admin_id} не является администратором.")
- return
-
- # Удаляем администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Удаляем ID из строки с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
- new_ids = [id for id in current_ids if int(id) != admin_id]
-
- if not new_ids:
- # Если не осталось администраторов, добавляем текущего пользователя
- # чтобы избежать ситуации, когда нет администраторов
- new_ids = [str(update.effective_user.id)]
-
- lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.remove(admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {admin_id} удален из списка администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
- else:
- await update.message.reply_text("❌ Ошибка: Не удалось найти настройки администраторов.")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error removing admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при удалении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Показывает список администраторов бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- try:
- if not ADMIN_USER_IDS:
- await update.message.reply_text("⚠️ Список администраторов пуст.")
- return
-
- # Формируем сообщение со списком администраторов
- admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_USER_IDS])
-
- await update.message.reply_text(
- f"👑 Список администраторов бота:\n\n"
- f"{admin_list}\n\n"
- f"Всего администраторов: {len(ADMIN_USER_IDS)}",
- parse_mode="HTML"
- )
-
- except Exception as e:
- logger.error(f"Error listing admins: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при получении списка администраторов:\n\n{str(e)}",
- parse_mode="HTML"
- )
diff --git a/.history/src/utils/admin_utils_20250830142452.py b/.history/src/utils/admin_utils_20250830142452.py
deleted file mode 100644
index ed11249..0000000
--- a/.history/src/utils/admin_utils_20250830142452.py
+++ /dev/null
@@ -1,301 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Утилиты для управления администраторами бота
-"""
-
-import os
-import logging
-from typing import List, Optional, Callable, Any, Union
-from functools import wraps
-from telegram import Update
-from telegram.ext import ContextTypes
-from src.config.config import ADMIN_USER_IDS
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-def is_admin(user_id: int) -> bool:
- """Проверяет, является ли пользователь администратором бота
-
- Args:
- user_id: ID пользователя Telegram
-
- Returns:
- True если пользователь администратор, иначе False
- """
- return user_id in ADMIN_USER_IDS
-
-def admin_required(func: Callable) -> Callable:
- """Декоратор для проверки, является ли пользователь администратором
-
- Args:
- func: Оригинальная функция обработчика
-
- Returns:
- Обернутая функция с проверкой прав администратора
- """
- @wraps(func)
- async def wrapper(*args: Any, **kwargs: Any) -> Any:
- # Определяем, является ли функция методом класса
- # Если первый аргумент - это self, то второй должен быть update
- if len(args) >= 2 and isinstance(args[1], Update):
- self_obj = args[0]
- update = args[1]
- context = args[2] if len(args) > 2 else kwargs.get('context')
- else:
- # Если это обычная функция, то первый аргумент - update
- update = args[0] if args else kwargs.get('update')
- context = args[1] if len(args) > 1 else kwargs.get('context')
- self_obj = None
-
- # Проверяем доступность объекта update и effective_user
- if not update or not update.effective_user:
- logger.warning("Update object is incomplete, unable to check admin status")
- return
-
- user_id = update.effective_user.id
- username = update.effective_user.username or "Unknown"
-
- if not is_admin(user_id):
- logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
-
- # Если это сообщение, отправляем уведомление
- if update.message:
- await update.message.reply_text(
- "⛔️ У вас нет прав на использование этой команды.\n"
- "Обратитесь к владельцу бота, чтобы получить доступ."
- )
- # Если это callback query, отвечаем на него
- elif update.callback_query:
- await update.callback_query.answer(
- "⛔️ У вас нет прав на использование этой функции."
- )
- return
-
- # Если пользователь админ, вызываем оригинальную функцию
- return await func(*args, **kwargs)
-
- return wrapper
-
-async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Добавляет нового администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID нового администратора
- new_admin_id = int(context.args[0])
-
- # Проверяем, не является ли пользователь уже администратором
- if new_admin_id in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {new_admin_id} уже является администратором.")
- return
-
- # Добавляем нового администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Обновляем или добавляем строку с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip()
- new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
- lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
- else:
- lines.append(f"ADMIN_USER_IDS={new_admin_id}")
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.append(new_admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {new_admin_id} добавлен в список администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error adding admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при добавлении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Удаляет администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID администратора для удаления
- admin_id = int(context.args[0])
-
- # Проверяем, не удаляет ли админ сам себя
- if admin_id == update.effective_user.id:
- await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
- return
-
- # Проверяем, является ли пользователь администратором
- if admin_id not in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {admin_id} не является администратором.")
- return
-
- # Удаляем администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Удаляем ID из строки с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
- new_ids = [id for id in current_ids if int(id) != admin_id]
-
- if not new_ids:
- # Если не осталось администраторов, добавляем текущего пользователя
- # чтобы избежать ситуации, когда нет администраторов
- new_ids = [str(update.effective_user.id)]
-
- lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.remove(admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {admin_id} удален из списка администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
- else:
- await update.message.reply_text("❌ Ошибка: Не удалось найти настройки администраторов.")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error removing admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при удалении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Показывает список администраторов бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- try:
- if not ADMIN_USER_IDS:
- await update.message.reply_text("⚠️ Список администраторов пуст.")
- return
-
- # Формируем сообщение со списком администраторов
- admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_USER_IDS])
-
- await update.message.reply_text(
- f"👑 Список администраторов бота:\n\n"
- f"{admin_list}\n\n"
- f"Всего администраторов: {len(ADMIN_USER_IDS)}",
- parse_mode="HTML"
- )
-
- except Exception as e:
- logger.error(f"Error listing admins: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при получении списка администраторов:\n\n{str(e)}",
- parse_mode="HTML"
- )
diff --git a/.history/src/utils/admin_utils_20250830142546.py b/.history/src/utils/admin_utils_20250830142546.py
deleted file mode 100644
index d3639c4..0000000
--- a/.history/src/utils/admin_utils_20250830142546.py
+++ /dev/null
@@ -1,303 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Утилиты для управления администраторами бота
-"""
-
-import os
-import logging
-from typing import List, Optional, Callable, Any, Union
-from functools import wraps
-from telegram import Update
-from telegram.ext import ContextTypes
-from src.config.config import ADMIN_USER_IDS
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-def is_admin(user_id: int) -> bool:
- """Проверяет, является ли пользователь администратором бота
-
- Args:
- user_id: ID пользователя Telegram
-
- Returns:
- True если пользователь администратор, иначе False
- """
- return user_id in ADMIN_USER_IDS
-
-def admin_required(func: Callable) -> Callable:
- """Декоратор для проверки, является ли пользователь администратором
-
- Args:
- func: Оригинальная функция обработчика
-
- Returns:
- Обернутая функция с проверкой прав администратора
- """
- @wraps(func)
- async def wrapper(*args: Any, **kwargs: Any) -> Any:
- # Определяем, является ли функция методом класса
- # Если первый аргумент - это self, то второй должен быть update
- if len(args) >= 2 and isinstance(args[1], Update):
- self_obj = args[0]
- update = args[1]
- context = args[2] if len(args) > 2 else kwargs.get('context')
- else:
- # Если это обычная функция, то первый аргумент - update
- update = args[0] if args else kwargs.get('update')
- context = args[1] if len(args) > 1 else kwargs.get('context')
- self_obj = None
-
- # Проверяем доступность объекта update и effective_user
- if not update or not update.effective_user:
- logger.warning("Update object is incomplete, unable to check admin status")
- return
-
- user_id = update.effective_user.id
- username = update.effective_user.username or "Unknown"
-
- if not is_admin(user_id):
- logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
-
- # Если это сообщение, отправляем уведомление
- if update.message:
- await update.message.reply_text(
- "⛔️ У вас нет прав на использование этой команды.\n"
- "Обратитесь к владельцу бота, чтобы получить доступ."
- )
- # Если это callback query, отвечаем на него
- elif update.callback_query:
- await update.callback_query.answer(
- "⛔️ У вас нет прав на использование этой функции."
- )
- return
-
- # Если пользователь админ, вызываем оригинальную функцию
- return await func(*args, **kwargs)
-
- return wrapper
-
-async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Добавляет нового администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID нового администратора
- new_admin_id = int(context.args[0])
-
- # Проверяем, не является ли пользователь уже администратором
- if new_admin_id in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {new_admin_id} уже является администратором.")
- return
-
- # Добавляем нового администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Обновляем или добавляем строку с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip()
- new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
- lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
- else:
- lines.append(f"ADMIN_USER_IDS={new_admin_id}")
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.append(new_admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {new_admin_id} добавлен в список администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error adding admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при добавлении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Удаляет администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- if update.message:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID администратора для удаления
- admin_id = int(context.args[0])
-
- # Проверяем, не удаляет ли админ сам себя
- if admin_id == update.effective_user.id:
- await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
- return
-
- # Проверяем, является ли пользователь администратором
- if admin_id not in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {admin_id} не является администратором.")
- return
-
- # Удаляем администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Удаляем ID из строки с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
- new_ids = [id for id in current_ids if int(id) != admin_id]
-
- if not new_ids:
- # Если не осталось администраторов, добавляем текущего пользователя
- # чтобы избежать ситуации, когда нет администраторов
- new_ids = [str(update.effective_user.id)]
-
- lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.remove(admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {admin_id} удален из списка администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
- else:
- await update.message.reply_text("❌ Ошибка: Не удалось найти настройки администраторов.")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error removing admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при удалении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Показывает список администраторов бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- try:
- if not ADMIN_USER_IDS:
- await update.message.reply_text("⚠️ Список администраторов пуст.")
- return
-
- # Формируем сообщение со списком администраторов
- admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_USER_IDS])
-
- await update.message.reply_text(
- f"👑 Список администраторов бота:\n\n"
- f"{admin_list}\n\n"
- f"Всего администраторов: {len(ADMIN_USER_IDS)}",
- parse_mode="HTML"
- )
-
- except Exception as e:
- logger.error(f"Error listing admins: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при получении списка администраторов:\n\n{str(e)}",
- parse_mode="HTML"
- )
diff --git a/.history/src/utils/admin_utils_20250830142616.py b/.history/src/utils/admin_utils_20250830142616.py
deleted file mode 100644
index 24ec801..0000000
--- a/.history/src/utils/admin_utils_20250830142616.py
+++ /dev/null
@@ -1,310 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Утилиты для управления администраторами бота
-"""
-
-import os
-import logging
-from typing import List, Optional, Callable, Any, Union
-from functools import wraps
-from telegram import Update
-from telegram.ext import ContextTypes
-from src.config.config import ADMIN_USER_IDS
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-def is_admin(user_id: int) -> bool:
- """Проверяет, является ли пользователь администратором бота
-
- Args:
- user_id: ID пользователя Telegram
-
- Returns:
- True если пользователь администратор, иначе False
- """
- return user_id in ADMIN_USER_IDS
-
-def admin_required(func: Callable) -> Callable:
- """Декоратор для проверки, является ли пользователь администратором
-
- Args:
- func: Оригинальная функция обработчика
-
- Returns:
- Обернутая функция с проверкой прав администратора
- """
- @wraps(func)
- async def wrapper(*args: Any, **kwargs: Any) -> Any:
- # Определяем, является ли функция методом класса
- # Если первый аргумент - это self, то второй должен быть update
- if len(args) >= 2 and isinstance(args[1], Update):
- self_obj = args[0]
- update = args[1]
- context = args[2] if len(args) > 2 else kwargs.get('context')
- else:
- # Если это обычная функция, то первый аргумент - update
- update = args[0] if args else kwargs.get('update')
- context = args[1] if len(args) > 1 else kwargs.get('context')
- self_obj = None
-
- # Проверяем доступность объекта update и effective_user
- if not update or not update.effective_user:
- logger.warning("Update object is incomplete, unable to check admin status")
- return
-
- user_id = update.effective_user.id
- username = update.effective_user.username or "Unknown"
-
- if not is_admin(user_id):
- logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
-
- # Если это сообщение, отправляем уведомление
- if update.message:
- await update.message.reply_text(
- "⛔️ У вас нет прав на использование этой команды.\n"
- "Обратитесь к владельцу бота, чтобы получить доступ."
- )
- # Если это callback query, отвечаем на него
- elif update.callback_query:
- await update.callback_query.answer(
- "⛔️ У вас нет прав на использование этой функции."
- )
- return
-
- # Если пользователь админ, вызываем оригинальную функцию
- return await func(*args, **kwargs)
-
- return wrapper
-
-async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Добавляет нового администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID нового администратора
- new_admin_id = int(context.args[0])
-
- # Проверяем, не является ли пользователь уже администратором
- if new_admin_id in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {new_admin_id} уже является администратором.")
- return
-
- # Добавляем нового администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Обновляем или добавляем строку с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip()
- new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
- lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
- else:
- lines.append(f"ADMIN_USER_IDS={new_admin_id}")
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.append(new_admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {new_admin_id} добавлен в список администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error adding admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при добавлении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Удаляет администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- if update.message:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID администратора для удаления
- admin_id = int(context.args[0])
-
- # Проверяем, не удаляет ли админ сам себя
- if admin_id == update.effective_user.id:
- await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
- return
-
- # Проверяем, является ли пользователь администратором
- if admin_id not in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {admin_id} не является администратором.")
- return
-
- # Удаляем администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Удаляем ID из строки с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
- new_ids = [id for id in current_ids if int(id) != admin_id]
-
- if not new_ids:
- # Если не осталось администраторов, добавляем текущего пользователя
- # чтобы избежать ситуации, когда нет администраторов
- new_ids = [str(update.effective_user.id)]
-
- lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.remove(admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {admin_id} удален из списка администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
- else:
- await update.message.reply_text("❌ Ошибка: Не удалось найти настройки администраторов.")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error removing admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при удалении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Показывает список администраторов бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- try:
- if not ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⚠️ Список администраторов пуст.")
- return
-
- # Формируем сообщение со списком администраторов
- admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_USER_IDS])
-
- if update.message:
- await update.message.reply_text(
- f"👑 Список администраторов бота:\n\n"
- f"{admin_list}\n\n"
- f"Всего администраторов: {len(ADMIN_USER_IDS)}",
- parse_mode="HTML"
- )
-
- except Exception as e:
- logger.error(f"Error listing admins: {str(e)}")
- if update.message:
- await update.message.reply_text(
- f"❌ Произошла ошибка при получении списка администраторов:\n\n{str(e)}",
- parse_mode="HTML"
- )
diff --git a/.history/src/utils/admin_utils_20250830142633.py b/.history/src/utils/admin_utils_20250830142633.py
deleted file mode 100644
index 194e990..0000000
--- a/.history/src/utils/admin_utils_20250830142633.py
+++ /dev/null
@@ -1,312 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Утилиты для управления администраторами бота
-"""
-
-import os
-import logging
-from typing import List, Optional, Callable, Any, Union
-from functools import wraps
-from telegram import Update
-from telegram.ext import ContextTypes
-from src.config.config import ADMIN_USER_IDS
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-def is_admin(user_id: int) -> bool:
- """Проверяет, является ли пользователь администратором бота
-
- Args:
- user_id: ID пользователя Telegram
-
- Returns:
- True если пользователь администратор, иначе False
- """
- return user_id in ADMIN_USER_IDS
-
-def admin_required(func: Callable) -> Callable:
- """Декоратор для проверки, является ли пользователь администратором
-
- Args:
- func: Оригинальная функция обработчика
-
- Returns:
- Обернутая функция с проверкой прав администратора
- """
- @wraps(func)
- async def wrapper(*args: Any, **kwargs: Any) -> Any:
- # Определяем, является ли функция методом класса
- # Если первый аргумент - это self, то второй должен быть update
- if len(args) >= 2 and isinstance(args[1], Update):
- self_obj = args[0]
- update = args[1]
- context = args[2] if len(args) > 2 else kwargs.get('context')
- else:
- # Если это обычная функция, то первый аргумент - update
- update = args[0] if args else kwargs.get('update')
- context = args[1] if len(args) > 1 else kwargs.get('context')
- self_obj = None
-
- # Проверяем доступность объекта update и effective_user
- if not update or not update.effective_user:
- logger.warning("Update object is incomplete, unable to check admin status")
- return
-
- user_id = update.effective_user.id
- username = update.effective_user.username or "Unknown"
-
- if not is_admin(user_id):
- logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
-
- # Если это сообщение, отправляем уведомление
- if update.message:
- await update.message.reply_text(
- "⛔️ У вас нет прав на использование этой команды.\n"
- "Обратитесь к владельцу бота, чтобы получить доступ."
- )
- # Если это callback query, отвечаем на него
- elif update.callback_query:
- await update.callback_query.answer(
- "⛔️ У вас нет прав на использование этой функции."
- )
- return
-
- # Если пользователь админ, вызываем оригинальную функцию
- return await func(*args, **kwargs)
-
- return wrapper
-
-async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Добавляет нового администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID нового администратора
- new_admin_id = int(context.args[0])
-
- # Проверяем, не является ли пользователь уже администратором
- if new_admin_id in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {new_admin_id} уже является администратором.")
- return
-
- # Добавляем нового администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Обновляем или добавляем строку с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip()
- new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
- lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
- else:
- lines.append(f"ADMIN_USER_IDS={new_admin_id}")
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.append(new_admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {new_admin_id} добавлен в список администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error adding admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при добавлении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Удаляет администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- if update.message:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID администратора для удаления
- admin_id = int(context.args[0])
-
- # Проверяем, не удаляет ли админ сам себя
- if admin_id == update.effective_user.id:
- if update.message:
- await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
- return
-
- # Проверяем, является ли пользователь администратором
- if admin_id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {admin_id} не является администратором.")
- return
-
- # Удаляем администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Удаляем ID из строки с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
- new_ids = [id for id in current_ids if int(id) != admin_id]
-
- if not new_ids:
- # Если не осталось администраторов, добавляем текущего пользователя
- # чтобы избежать ситуации, когда нет администраторов
- new_ids = [str(update.effective_user.id)]
-
- lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.remove(admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {admin_id} удален из списка администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
- else:
- await update.message.reply_text("❌ Ошибка: Не удалось найти настройки администраторов.")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error removing admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при удалении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Показывает список администраторов бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- try:
- if not ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⚠️ Список администраторов пуст.")
- return
-
- # Формируем сообщение со списком администраторов
- admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_USER_IDS])
-
- if update.message:
- await update.message.reply_text(
- f"👑 Список администраторов бота:\n\n"
- f"{admin_list}\n\n"
- f"Всего администраторов: {len(ADMIN_USER_IDS)}",
- parse_mode="HTML"
- )
-
- except Exception as e:
- logger.error(f"Error listing admins: {str(e)}")
- if update.message:
- await update.message.reply_text(
- f"❌ Произошла ошибка при получении списка администраторов:\n\n{str(e)}",
- parse_mode="HTML"
- )
diff --git a/.history/src/utils/admin_utils_20250830142645.py b/.history/src/utils/admin_utils_20250830142645.py
deleted file mode 100644
index fca3a38..0000000
--- a/.history/src/utils/admin_utils_20250830142645.py
+++ /dev/null
@@ -1,314 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Утилиты для управления администраторами бота
-"""
-
-import os
-import logging
-from typing import List, Optional, Callable, Any, Union
-from functools import wraps
-from telegram import Update
-from telegram.ext import ContextTypes
-from src.config.config import ADMIN_USER_IDS
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-def is_admin(user_id: int) -> bool:
- """Проверяет, является ли пользователь администратором бота
-
- Args:
- user_id: ID пользователя Telegram
-
- Returns:
- True если пользователь администратор, иначе False
- """
- return user_id in ADMIN_USER_IDS
-
-def admin_required(func: Callable) -> Callable:
- """Декоратор для проверки, является ли пользователь администратором
-
- Args:
- func: Оригинальная функция обработчика
-
- Returns:
- Обернутая функция с проверкой прав администратора
- """
- @wraps(func)
- async def wrapper(*args: Any, **kwargs: Any) -> Any:
- # Определяем, является ли функция методом класса
- # Если первый аргумент - это self, то второй должен быть update
- if len(args) >= 2 and isinstance(args[1], Update):
- self_obj = args[0]
- update = args[1]
- context = args[2] if len(args) > 2 else kwargs.get('context')
- else:
- # Если это обычная функция, то первый аргумент - update
- update = args[0] if args else kwargs.get('update')
- context = args[1] if len(args) > 1 else kwargs.get('context')
- self_obj = None
-
- # Проверяем доступность объекта update и effective_user
- if not update or not update.effective_user:
- logger.warning("Update object is incomplete, unable to check admin status")
- return
-
- user_id = update.effective_user.id
- username = update.effective_user.username or "Unknown"
-
- if not is_admin(user_id):
- logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
-
- # Если это сообщение, отправляем уведомление
- if update.message:
- await update.message.reply_text(
- "⛔️ У вас нет прав на использование этой команды.\n"
- "Обратитесь к владельцу бота, чтобы получить доступ."
- )
- # Если это callback query, отвечаем на него
- elif update.callback_query:
- await update.callback_query.answer(
- "⛔️ У вас нет прав на использование этой функции."
- )
- return
-
- # Если пользователь админ, вызываем оригинальную функцию
- return await func(*args, **kwargs)
-
- return wrapper
-
-async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Добавляет нового администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID нового администратора
- new_admin_id = int(context.args[0])
-
- # Проверяем, не является ли пользователь уже администратором
- if new_admin_id in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {new_admin_id} уже является администратором.")
- return
-
- # Добавляем нового администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Обновляем или добавляем строку с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip()
- new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
- lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
- else:
- lines.append(f"ADMIN_USER_IDS={new_admin_id}")
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.append(new_admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {new_admin_id} добавлен в список администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error adding admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при добавлении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Удаляет администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- if update.message:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID администратора для удаления
- admin_id = int(context.args[0])
-
- # Проверяем, не удаляет ли админ сам себя
- if admin_id == update.effective_user.id:
- if update.message:
- await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
- return
-
- # Проверяем, является ли пользователь администратором
- if admin_id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {admin_id} не является администратором.")
- return
-
- # Удаляем администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Удаляем ID из строки с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
- new_ids = [id for id in current_ids if int(id) != admin_id]
-
- if not new_ids:
- # Если не осталось администраторов, добавляем текущего пользователя
- # чтобы избежать ситуации, когда нет администраторов
- new_ids = [str(update.effective_user.id)]
-
- lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.remove(admin_id)
-
- if update.message:
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {admin_id} удален из списка администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
- else:
- if update.message:
- await update.message.reply_text("❌ Ошибка: Не удалось найти настройки администраторов.")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error removing admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при удалении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Показывает список администраторов бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- try:
- if not ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⚠️ Список администраторов пуст.")
- return
-
- # Формируем сообщение со списком администраторов
- admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_USER_IDS])
-
- if update.message:
- await update.message.reply_text(
- f"👑 Список администраторов бота:\n\n"
- f"{admin_list}\n\n"
- f"Всего администраторов: {len(ADMIN_USER_IDS)}",
- parse_mode="HTML"
- )
-
- except Exception as e:
- logger.error(f"Error listing admins: {str(e)}")
- if update.message:
- await update.message.reply_text(
- f"❌ Произошла ошибка при получении списка администраторов:\n\n{str(e)}",
- parse_mode="HTML"
- )
diff --git a/.history/src/utils/admin_utils_20250830142656.py b/.history/src/utils/admin_utils_20250830142656.py
deleted file mode 100644
index 61a792d..0000000
--- a/.history/src/utils/admin_utils_20250830142656.py
+++ /dev/null
@@ -1,317 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Утилиты для управления администраторами бота
-"""
-
-import os
-import logging
-from typing import List, Optional, Callable, Any, Union
-from functools import wraps
-from telegram import Update
-from telegram.ext import ContextTypes
-from src.config.config import ADMIN_USER_IDS
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-def is_admin(user_id: int) -> bool:
- """Проверяет, является ли пользователь администратором бота
-
- Args:
- user_id: ID пользователя Telegram
-
- Returns:
- True если пользователь администратор, иначе False
- """
- return user_id in ADMIN_USER_IDS
-
-def admin_required(func: Callable) -> Callable:
- """Декоратор для проверки, является ли пользователь администратором
-
- Args:
- func: Оригинальная функция обработчика
-
- Returns:
- Обернутая функция с проверкой прав администратора
- """
- @wraps(func)
- async def wrapper(*args: Any, **kwargs: Any) -> Any:
- # Определяем, является ли функция методом класса
- # Если первый аргумент - это self, то второй должен быть update
- if len(args) >= 2 and isinstance(args[1], Update):
- self_obj = args[0]
- update = args[1]
- context = args[2] if len(args) > 2 else kwargs.get('context')
- else:
- # Если это обычная функция, то первый аргумент - update
- update = args[0] if args else kwargs.get('update')
- context = args[1] if len(args) > 1 else kwargs.get('context')
- self_obj = None
-
- # Проверяем доступность объекта update и effective_user
- if not update or not update.effective_user:
- logger.warning("Update object is incomplete, unable to check admin status")
- return
-
- user_id = update.effective_user.id
- username = update.effective_user.username or "Unknown"
-
- if not is_admin(user_id):
- logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
-
- # Если это сообщение, отправляем уведомление
- if update.message:
- await update.message.reply_text(
- "⛔️ У вас нет прав на использование этой команды.\n"
- "Обратитесь к владельцу бота, чтобы получить доступ."
- )
- # Если это callback query, отвечаем на него
- elif update.callback_query:
- await update.callback_query.answer(
- "⛔️ У вас нет прав на использование этой функции."
- )
- return
-
- # Если пользователь админ, вызываем оригинальную функцию
- return await func(*args, **kwargs)
-
- return wrapper
-
-async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Добавляет нового администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID нового администратора
- new_admin_id = int(context.args[0])
-
- # Проверяем, не является ли пользователь уже администратором
- if new_admin_id in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {new_admin_id} уже является администратором.")
- return
-
- # Добавляем нового администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Обновляем или добавляем строку с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip()
- new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
- lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
- else:
- lines.append(f"ADMIN_USER_IDS={new_admin_id}")
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.append(new_admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {new_admin_id} добавлен в список администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error adding admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при добавлении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Удаляет администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- if update.message:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID администратора для удаления
- admin_id = int(context.args[0])
-
- # Проверяем, не удаляет ли админ сам себя
- if admin_id == update.effective_user.id:
- if update.message:
- await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
- return
-
- # Проверяем, является ли пользователь администратором
- if admin_id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {admin_id} не является администратором.")
- return
-
- # Удаляем администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Удаляем ID из строки с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
- new_ids = [id for id in current_ids if int(id) != admin_id]
-
- if not new_ids:
- # Если не осталось администраторов, добавляем текущего пользователя
- # чтобы избежать ситуации, когда нет администраторов
- new_ids = [str(update.effective_user.id)]
-
- lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.remove(admin_id)
-
- if update.message:
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {admin_id} удален из списка администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
- else:
- if update.message:
- await update.message.reply_text("❌ Ошибка: Не удалось найти настройки администраторов.")
-
- except ValueError:
- if update.message:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error removing admin: {str(e)}")
- if update.message:
- await update.message.reply_text(f"❌ Произошла ошибка:\n\n{str(e)}", parse_mode="HTML")
- await update.message.reply_text(
- f"❌ Произошла ошибка при удалении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Показывает список администраторов бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- try:
- if not ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⚠️ Список администраторов пуст.")
- return
-
- # Формируем сообщение со списком администраторов
- admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_USER_IDS])
-
- if update.message:
- await update.message.reply_text(
- f"👑 Список администраторов бота:\n\n"
- f"{admin_list}\n\n"
- f"Всего администраторов: {len(ADMIN_USER_IDS)}",
- parse_mode="HTML"
- )
-
- except Exception as e:
- logger.error(f"Error listing admins: {str(e)}")
- if update.message:
- await update.message.reply_text(
- f"❌ Произошла ошибка при получении списка администраторов:\n\n{str(e)}",
- parse_mode="HTML"
- )
diff --git a/.history/src/utils/admin_utils_20250830143155.py b/.history/src/utils/admin_utils_20250830143155.py
deleted file mode 100644
index 61a792d..0000000
--- a/.history/src/utils/admin_utils_20250830143155.py
+++ /dev/null
@@ -1,317 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Утилиты для управления администраторами бота
-"""
-
-import os
-import logging
-from typing import List, Optional, Callable, Any, Union
-from functools import wraps
-from telegram import Update
-from telegram.ext import ContextTypes
-from src.config.config import ADMIN_USER_IDS
-
-# Настройка логирования
-logger = logging.getLogger(__name__)
-
-def is_admin(user_id: int) -> bool:
- """Проверяет, является ли пользователь администратором бота
-
- Args:
- user_id: ID пользователя Telegram
-
- Returns:
- True если пользователь администратор, иначе False
- """
- return user_id in ADMIN_USER_IDS
-
-def admin_required(func: Callable) -> Callable:
- """Декоратор для проверки, является ли пользователь администратором
-
- Args:
- func: Оригинальная функция обработчика
-
- Returns:
- Обернутая функция с проверкой прав администратора
- """
- @wraps(func)
- async def wrapper(*args: Any, **kwargs: Any) -> Any:
- # Определяем, является ли функция методом класса
- # Если первый аргумент - это self, то второй должен быть update
- if len(args) >= 2 and isinstance(args[1], Update):
- self_obj = args[0]
- update = args[1]
- context = args[2] if len(args) > 2 else kwargs.get('context')
- else:
- # Если это обычная функция, то первый аргумент - update
- update = args[0] if args else kwargs.get('update')
- context = args[1] if len(args) > 1 else kwargs.get('context')
- self_obj = None
-
- # Проверяем доступность объекта update и effective_user
- if not update or not update.effective_user:
- logger.warning("Update object is incomplete, unable to check admin status")
- return
-
- user_id = update.effective_user.id
- username = update.effective_user.username or "Unknown"
-
- if not is_admin(user_id):
- logger.warning(f"Unauthorized access attempt by user {user_id} (@{username})")
-
- # Если это сообщение, отправляем уведомление
- if update.message:
- await update.message.reply_text(
- "⛔️ У вас нет прав на использование этой команды.\n"
- "Обратитесь к владельцу бота, чтобы получить доступ."
- )
- # Если это callback query, отвечаем на него
- elif update.callback_query:
- await update.callback_query.answer(
- "⛔️ У вас нет прав на использование этой функции."
- )
- return
-
- # Если пользователь админ, вызываем оригинальную функцию
- return await func(*args, **kwargs)
-
- return wrapper
-
-async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Добавляет нового администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID нового администратора
- new_admin_id = int(context.args[0])
-
- # Проверяем, не является ли пользователь уже администратором
- if new_admin_id in ADMIN_USER_IDS:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {new_admin_id} уже является администратором.")
- return
-
- # Добавляем нового администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Обновляем или добавляем строку с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip()
- new_ids = f"{current_ids},{new_admin_id}" if current_ids else str(new_admin_id)
- lines[admin_line_idx] = f"ADMIN_USER_IDS={new_ids}"
- else:
- lines.append(f"ADMIN_USER_IDS={new_admin_id}")
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.append(new_admin_id)
-
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {new_admin_id} добавлен в список администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} added a new admin: {new_admin_id}")
-
- except ValueError:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/addadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error adding admin: {str(e)}")
- await update.message.reply_text(
- f"❌ Произошла ошибка при добавлении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Удаляет администратора бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- # Проверяем, есть ли аргументы команды
- if not context.args or len(context.args) < 1:
- if update.message:
- await update.message.reply_text(
- "❌ Ошибка: Необходимо указать ID пользователя.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- return
-
- try:
- # Парсим ID администратора для удаления
- admin_id = int(context.args[0])
-
- # Проверяем, не удаляет ли админ сам себя
- if admin_id == update.effective_user.id:
- if update.message:
- await update.message.reply_text("⚠️ Вы не можете удалить самого себя из администраторов.")
- return
-
- # Проверяем, является ли пользователь администратором
- if admin_id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text(f"ℹ️ Пользователь с ID {admin_id} не является администратором.")
- return
-
- # Удаляем администратора
- env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
-
- # Читаем текущий файл .env
- env_content = ""
- with open(env_path, 'r', encoding='utf-8') as f:
- env_content = f.read()
-
- # Находим строку с ADMIN_USER_IDS
- lines = env_content.split('\n')
- admin_line_idx = -1
-
- for i, line in enumerate(lines):
- if line.startswith('ADMIN_USER_IDS='):
- admin_line_idx = i
- break
-
- # Удаляем ID из строки с администраторами
- if admin_line_idx >= 0:
- current_ids = lines[admin_line_idx].split('=')[1].strip().split(',')
- new_ids = [id for id in current_ids if int(id) != admin_id]
-
- if not new_ids:
- # Если не осталось администраторов, добавляем текущего пользователя
- # чтобы избежать ситуации, когда нет администраторов
- new_ids = [str(update.effective_user.id)]
-
- lines[admin_line_idx] = f"ADMIN_USER_IDS={','.join(new_ids)}"
-
- # Записываем обновленный файл
- with open(env_path, 'w', encoding='utf-8') as f:
- f.write('\n'.join(lines))
-
- # Обновляем список в памяти
- ADMIN_USER_IDS.remove(admin_id)
-
- if update.message:
- await update.message.reply_text(
- f"✅ Успешно!\n\n"
- f"Пользователь с ID {admin_id} удален из списка администраторов.",
- parse_mode="HTML"
- )
-
- logger.info(f"User {update.effective_user.id} removed admin: {admin_id}")
- else:
- if update.message:
- await update.message.reply_text("❌ Ошибка: Не удалось найти настройки администраторов.")
-
- except ValueError:
- if update.message:
- await update.message.reply_text(
- "❌ Ошибка: ID пользователя должен быть целым числом.\n\n"
- "Пример использования:\n"
- "/removeadmin 123456789",
- parse_mode="HTML"
- )
- except Exception as e:
- logger.error(f"Error removing admin: {str(e)}")
- if update.message:
- await update.message.reply_text(f"❌ Произошла ошибка:\n\n{str(e)}", parse_mode="HTML")
- await update.message.reply_text(
- f"❌ Произошла ошибка при удалении администратора:\n\n{str(e)}",
- parse_mode="HTML"
- )
-
-async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Показывает список администраторов бота
-
- Args:
- update: Объект обновления Telegram
- context: Контекст вызова
- """
- if not update.message:
- return
-
- # Проверяем, что команду выполняет администратор
- if not update.effective_user or update.effective_user.id not in ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⛔️ У вас нет прав для выполнения этой команды.")
- return
-
- try:
- if not ADMIN_USER_IDS:
- if update.message:
- await update.message.reply_text("⚠️ Список администраторов пуст.")
- return
-
- # Формируем сообщение со списком администраторов
- admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_USER_IDS])
-
- if update.message:
- await update.message.reply_text(
- f"👑 Список администраторов бота:\n\n"
- f"{admin_list}\n\n"
- f"Всего администраторов: {len(ADMIN_USER_IDS)}",
- parse_mode="HTML"
- )
-
- except Exception as e:
- logger.error(f"Error listing admins: {str(e)}")
- if update.message:
- await update.message.reply_text(
- f"❌ Произошла ошибка при получении списка администраторов:\n\n{str(e)}",
- parse_mode="HTML"
- )
diff --git a/.history/src/utils/logger_20250830063702.py b/.history/src/utils/logger_20250830063702.py
deleted file mode 100644
index 3ecbddd..0000000
--- a/.history/src/utils/logger_20250830063702.py
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для настройки логирования
-"""
-
-import os
-import logging
-from logging.handlers import RotatingFileHandler
-
-def setup_logging(log_level=logging.INFO) -> None:
- """
- Настройка логирования с ротацией файлов
-
- Args:
- log_level: Уровень логирования (по умолчанию INFO)
- """
- # Создание директории для логов, если её нет
- logs_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs")
- os.makedirs(logs_dir, exist_ok=True)
-
- # Путь к файлу лога
- log_file = os.path.join(logs_dir, "synology_bot.log")
-
- # Базовая настройка логгера
- logging.basicConfig(
- level=log_level,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
- handlers=[
- RotatingFileHandler(
- log_file,
- maxBytes=10485760, # 10MB
- backupCount=3
- ),
- logging.StreamHandler()
- ]
- )
-
- # Снижаем уровень логирования для некоторых модулей
- logging.getLogger("httpx").setLevel(logging.WARNING)
- logging.getLogger("telegram").setLevel(logging.WARNING)
-
- # Логирование старта системы
- logging.info("Logging system initialized")
diff --git a/.history/src/utils/logger_20250830063839.py b/.history/src/utils/logger_20250830063839.py
deleted file mode 100644
index 3ecbddd..0000000
--- a/.history/src/utils/logger_20250830063839.py
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Модуль для настройки логирования
-"""
-
-import os
-import logging
-from logging.handlers import RotatingFileHandler
-
-def setup_logging(log_level=logging.INFO) -> None:
- """
- Настройка логирования с ротацией файлов
-
- Args:
- log_level: Уровень логирования (по умолчанию INFO)
- """
- # Создание директории для логов, если её нет
- logs_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs")
- os.makedirs(logs_dir, exist_ok=True)
-
- # Путь к файлу лога
- log_file = os.path.join(logs_dir, "synology_bot.log")
-
- # Базовая настройка логгера
- logging.basicConfig(
- level=log_level,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
- handlers=[
- RotatingFileHandler(
- log_file,
- maxBytes=10485760, # 10MB
- backupCount=3
- ),
- logging.StreamHandler()
- ]
- )
-
- # Снижаем уровень логирования для некоторых модулей
- logging.getLogger("httpx").setLevel(logging.WARNING)
- logging.getLogger("telegram").setLevel(logging.WARNING)
-
- # Логирование старта системы
- logging.info("Logging system initialized")
diff --git a/.history/test_api_headers_20250830084440.py b/.history/test_api_headers_20250830084440.py
deleted file mode 100644
index ad073d6..0000000
--- a/.history/test_api_headers_20250830084440.py
+++ /dev/null
@@ -1,391 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Тестовый скрипт для диагностики проблемы с использованием специальных заголовков
-"""
-
-import requests
-import logging
-import json
-import sys
-import os
-import urllib3
-import time
-import socket
-from requests.adapters import HTTPAdapter
-from urllib3.util import Retry
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-# Настройка логирования
-logging.basicConfig(
- level=logging.DEBUG,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-# Тестовые учетные данные (для примера)
-SYNOLOGY_HOST = "192.168.0.102"
-SYNOLOGY_PORT = 5000
-SYNOLOGY_USERNAME = "superadmin"
-SYNOLOGY_PASSWORD = "Cl0ud_1985!"
-SYNOLOGY_SECURE = False
-SYNOLOGY_TIMEOUT = 10
-
-def test_api_with_headers():
- """Тестирование API с использованием специальных заголовков для решения проблемы 119"""
-
- # Создаем сессию
- session = requests.Session()
- session.verify = False # Отключаем проверку SSL
-
- # Настройки повторных попыток
- retry_strategy = Retry(
- total=3,
- status_forcelist=[429, 500, 502, 503, 504],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.0
- )
- adapter = HTTPAdapter(max_retries=retry_strategy)
- session.mount("http://", adapter)
- session.mount("https://", adapter)
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- logger.info(f"Тестирование API с заголовками для {base_url}")
-
- # Тест 1: Получение SID с настройкой cookie и user-agent
- logger.info("Тест 1: Авторизация с настройкой cookie и user-agent")
-
- # Добавление пользовательских заголовков
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
-
- session.headers.update(custom_headers)
-
- try:
- # Определяем путь для авторизации
- auth_info_url = f"{base_url}/entry.cgi"
- auth_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- auth_info_response = session.get(auth_info_url, params=auth_info_params)
- auth_info_data = auth_info_response.json()
-
- if auth_info_data.get("success"):
- auth_info = auth_info_data.get("data", {}).get("SYNO.API.Auth", {})
- auth_path = auth_info.get("path", "auth.cgi")
- auth_max_version = auth_info.get("maxVersion", 6)
-
- logger.info(f"API авторизации: путь={auth_path}, макс. версия={auth_max_version}")
-
- # Используем версию 3 вместо 6 - тестирование на возможное решение проблемы
- auth_version = min(3, auth_max_version) # Пробуем более старую версию API
-
- # Выполняем авторизацию
- auth_url = f"{base_url}/{auth_path}"
- auth_params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "DirectHeaderTest",
- "format": "cookie"
- }
-
- if auth_version >= 6:
- auth_params["enable_syno_token"] = "yes"
-
- logger.info(f"Авторизация с использованием SYNO.API.Auth v{auth_version}")
- auth_response = session.get(auth_url, params=auth_params)
- auth_data = auth_response.json()
-
- if auth_data.get("success"):
- sid = auth_data.get("data", {}).get("sid")
- logger.info(f"Авторизация успешна! SID: {sid[:10]}...")
-
- # Теперь проверим, работает ли получение информации о системе
- # с настройкой cookie и заголовков
-
- # Сначала настроим куки для сохранения SID
- cookies = {
- 'id': sid,
- 'sid': sid
- }
- session.cookies.update(cookies)
-
- # Определяем путь для SYNO.DSM.Info
- info_info_url = f"{base_url}/entry.cgi"
- info_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.DSM.Info"
- }
-
- info_info_response = session.get(info_info_url, params=info_info_params)
- info_info_data = info_info_response.json()
-
- if info_info_data.get("success"):
- info_info = info_info_data.get("data", {}).get("SYNO.DSM.Info", {})
- info_path = info_info.get("path", "entry.cgi")
- info_max_version = info_info.get("maxVersion", 1)
- info_min_version = info_info.get("minVersion", 1)
-
- logger.info(f"API SYNO.DSM.Info: путь={info_path}, версия={info_min_version}-{info_max_version}")
-
- # Используем правильную версию API
- info_version = min(2, info_max_version)
-
- # Делаем запрос для получения информации о системе
- # с использованием sid как параметр запроса
- logger.info("Тест 2: Получение информации о системе с SID как параметром")
- info_url = f"{base_url}/{info_path}"
- info_params = {
- "api": "SYNO.DSM.Info",
- "version": str(info_version),
- "method": "getinfo",
- "_sid": sid
- }
-
- logger.info(f"Запрос информации с использованием SYNO.DSM.Info v{info_version}")
- info_response = session.get(info_url, params=info_params)
- info_data = info_response.json()
-
- if info_data.get("success"):
- logger.info("Успешно получена информация о системе!")
- logger.info(f"Данные: {json.dumps(info_data.get('data', {}), indent=2)}")
- else:
- error_code = info_data.get("error", {}).get("code", -1)
- logger.error(f"Не удалось получить информацию о системе. Ошибка: {error_code}")
-
- # Пробуем альтернативный способ
- logger.info("Тест 3: Попытка получить базовую информацию через SYNO.Core.System")
-
- # Определяем путь для SYNO.Core.System
- system_info_url = f"{base_url}/entry.cgi"
- system_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System"
- }
-
- system_info_response = session.get(system_info_url, params=system_info_params)
- system_info_data = system_info_response.json()
-
- if system_info_data.get("success"):
- system_info = system_info_data.get("data", {}).get("SYNO.Core.System", {})
- system_path = system_info.get("path", "entry.cgi")
- system_max_version = system_info.get("maxVersion", 1)
- system_min_version = system_info.get("minVersion", 1)
-
- logger.info(f"API SYNO.Core.System: путь={system_path}, версия={system_min_version}-{system_max_version}")
-
- # Используем правильную версию API
- system_version = 1
-
- # Пробуем альтернативную стратегию с X-SYNO-TOKEN
- # Некоторые API Synology требуют специальный токен в заголовках
- token = auth_data.get("data", {}).get("synotoken")
- if token:
- session.headers.update({'X-SYNO-TOKEN': token})
- logger.info(f"Добавлен X-SYNO-TOKEN: {token}")
-
- # Делаем запрос для получения информации о системе
- system_url = f"{base_url}/{system_path}"
- system_params = {
- "api": "SYNO.Core.System",
- "version": str(system_version),
- "method": "info",
- "_sid": sid
- }
-
- logger.info(f"Запрос информации с использованием SYNO.Core.System v{system_version}")
- system_response = session.get(system_url, params=system_params)
- system_data = system_response.json()
-
- if system_data.get("success"):
- logger.info("Успешно получена информация о системе через SYNO.Core.System!")
- logger.info(f"Данные: {json.dumps(system_data.get('data', {}), indent=2)}")
- else:
- error_code = system_data.get("error", {}).get("code", -1)
- logger.error(f"Не удалось получить информацию через SYNO.Core.System. Ошибка: {error_code}")
-
- # Пробуем другие методы для диагностики
- logger.info("Тест 4: Попытка использовать различные методы и заголовки")
-
- # Пробуем создать полностью новую сессию
- new_session = requests.Session()
- new_session.verify = False
- new_session.headers.update(custom_headers)
-
- # Пробуем версию 1 для авторизации
- auth_version = 1
- auth_params["version"] = str(auth_version)
-
- auth_response = new_session.get(auth_url, params=auth_params)
- auth_data = auth_response.json()
-
- if auth_data.get("success"):
- sid = auth_data.get("data", {}).get("sid")
- logger.info(f"Новая авторизация (v{auth_version}) успешна! SID: {sid[:10]}...")
-
- # Добавляем SID как куки
- cookies = {
- 'id': sid,
- 'sid': sid
- }
- new_session.cookies.update(cookies)
-
- # Пробуем другой подход: разделение запросов во времени
- logger.info("Тест 5: Разделение запросов во времени")
-
- # Даем некоторое время для инициализации сессии на сервере
- time.sleep(2)
-
- # Пробуем получить список файлов
- filestation_info_url = f"{base_url}/entry.cgi"
- filestation_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.FileStation.List"
- }
-
- filestation_info_response = new_session.get(filestation_info_url, params=filestation_info_params)
- filestation_info_data = filestation_info_response.json()
-
- if filestation_info_data.get("success"):
- filestation_info = filestation_info_data.get("data", {}).get("SYNO.FileStation.List", {})
- filestation_path = filestation_info.get("path", "entry.cgi")
- filestation_max_version = filestation_info.get("maxVersion", 1)
-
- logger.info(f"API SYNO.FileStation.List: путь={filestation_path}, макс. версия={filestation_max_version}")
-
- # Используем правильную версию API
- filestation_version = min(2, filestation_max_version)
-
- # Делаем запрос для получения списка общих папок
- filestation_url = f"{base_url}/{filestation_path}"
- filestation_params = {
- "api": "SYNO.FileStation.List",
- "version": str(filestation_version),
- "method": "list_share",
- "_sid": sid
- }
-
- logger.info(f"Запрос списка общих папок с использованием SYNO.FileStation.List v{filestation_version}")
- filestation_response = new_session.get(filestation_url, params=filestation_params)
- filestation_data = filestation_response.json()
-
- if filestation_data.get("success"):
- logger.info("Успешно получен список общих папок!")
- shares = filestation_data.get("data", {}).get("shares", [])
- logger.info(f"Общие папки: {json.dumps(shares, indent=2)[:200]}...")
- else:
- error_code = filestation_data.get("error", {}).get("code", -1)
- logger.error(f"Не удалось получить список общих папок. Ошибка: {error_code}")
- else:
- error_code = auth_data.get("error", {}).get("code", -1)
- logger.error(f"Новая авторизация не удалась! Код ошибки: {error_code}")
- else:
- logger.error("Не удалось получить информацию о SYNO.Core.System API")
- else:
- logger.error("Не удалось получить информацию о SYNO.DSM.Info API")
- else:
- error_code = auth_data.get("error", {}).get("code", -1)
- logger.error(f"Авторизация не удалась! Код ошибки: {error_code}")
- else:
- logger.error("Не удалось получить информацию об API авторизации")
-
- except Exception as e:
- logger.error(f"Произошла ошибка: {str(e)}")
-
- # Тест 6: Проверка сетевой доступности
- logger.info("Тест 6: Проверка сетевой доступности")
-
- try:
- # Проверка базового TCP-соединения
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- if result == 0:
- logger.info("TCP-соединение успешно установлено")
- else:
- logger.error(f"Не удалось установить TCP-соединение, код ошибки: {result}")
- except Exception as e:
- logger.error(f"Ошибка при проверке TCP-соединения: {str(e)}")
-
- # Тест 7: Запрос без аутентификации для проверки доступности API
- logger.info("Тест 7: Запрос без аутентификации для проверки доступности API")
-
- try:
- # Создаем новую сессию без аутентификации
- simple_session = requests.Session()
- simple_session.verify = False
-
- # Запрос к SYNO.API.Info не требует аутентификации
- api_info_url = f"{base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "all"
- }
-
- logger.info("Запрос информации о всех API без аутентификации")
- api_info_response = simple_session.get(api_info_url, params=api_info_params)
-
- if api_info_response.status_code == 200:
- logger.info("API доступно без аутентификации")
- api_info_data = api_info_response.json()
-
- if api_info_data.get("success"):
- logger.info("Успешно получена информация о всех API")
- api_count = len(api_info_data.get("data", {}))
- logger.info(f"Количество доступных API: {api_count}")
-
- # Поиск API для управления питанием
- power_apis = []
- for api_name, api_info in api_info_data.get("data", {}).items():
- if "power" in api_name.lower() or "reboot" in api_name.lower() or "shutdown" in api_name.lower():
- power_apis.append(f"{api_name}: {api_info}")
-
- logger.info(f"Найдены API для управления питанием: {power_apis}")
-
- # Поиск API для получения информации о системе
- info_apis = []
- for api_name, api_info in api_info_data.get("data", {}).items():
- if "info" in api_name.lower() or "system" in api_name.lower() or "status" in api_name.lower():
- info_apis.append(f"{api_name}: {api_info}")
-
- logger.info(f"Найдены API для информации о системе: {info_apis[:5]} и еще {len(info_apis)-5}")
- else:
- error_code = api_info_data.get("error", {}).get("code", -1)
- logger.error(f"Запрос к API без аутентификации не удался! Код ошибки: {error_code}")
- else:
- logger.error(f"API не доступно без аутентификации. HTTP статус: {api_info_response.status_code}")
- except Exception as e:
- logger.error(f"Ошибка при проверке доступности API: {str(e)}")
-
-if __name__ == "__main__":
- logger.info("Запуск теста API с заголовками")
- test_api_with_headers()
diff --git a/.history/test_api_headers_20250830084500.py b/.history/test_api_headers_20250830084500.py
deleted file mode 100644
index ad073d6..0000000
--- a/.history/test_api_headers_20250830084500.py
+++ /dev/null
@@ -1,391 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Тестовый скрипт для диагностики проблемы с использованием специальных заголовков
-"""
-
-import requests
-import logging
-import json
-import sys
-import os
-import urllib3
-import time
-import socket
-from requests.adapters import HTTPAdapter
-from urllib3.util import Retry
-
-# Отключение предупреждений о небезопасных SSL-соединениях
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
-# Настройка логирования
-logging.basicConfig(
- level=logging.DEBUG,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-# Тестовые учетные данные (для примера)
-SYNOLOGY_HOST = "192.168.0.102"
-SYNOLOGY_PORT = 5000
-SYNOLOGY_USERNAME = "superadmin"
-SYNOLOGY_PASSWORD = "Cl0ud_1985!"
-SYNOLOGY_SECURE = False
-SYNOLOGY_TIMEOUT = 10
-
-def test_api_with_headers():
- """Тестирование API с использованием специальных заголовков для решения проблемы 119"""
-
- # Создаем сессию
- session = requests.Session()
- session.verify = False # Отключаем проверку SSL
-
- # Настройки повторных попыток
- retry_strategy = Retry(
- total=3,
- status_forcelist=[429, 500, 502, 503, 504],
- allowed_methods=["GET", "POST"],
- backoff_factor=1.0
- )
- adapter = HTTPAdapter(max_retries=retry_strategy)
- session.mount("http://", adapter)
- session.mount("https://", adapter)
-
- # Формируем базовый URL
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- logger.info(f"Тестирование API с заголовками для {base_url}")
-
- # Тест 1: Получение SID с настройкой cookie и user-agent
- logger.info("Тест 1: Авторизация с настройкой cookie и user-agent")
-
- # Добавление пользовательских заголовков
- custom_headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'application/json, text/javascript, */*; q=0.01',
- 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
- 'X-Requested-With': 'XMLHttpRequest',
- 'Connection': 'keep-alive',
- 'Referer': f'{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/'
- }
-
- session.headers.update(custom_headers)
-
- try:
- # Определяем путь для авторизации
- auth_info_url = f"{base_url}/entry.cgi"
- auth_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.API.Auth"
- }
-
- auth_info_response = session.get(auth_info_url, params=auth_info_params)
- auth_info_data = auth_info_response.json()
-
- if auth_info_data.get("success"):
- auth_info = auth_info_data.get("data", {}).get("SYNO.API.Auth", {})
- auth_path = auth_info.get("path", "auth.cgi")
- auth_max_version = auth_info.get("maxVersion", 6)
-
- logger.info(f"API авторизации: путь={auth_path}, макс. версия={auth_max_version}")
-
- # Используем версию 3 вместо 6 - тестирование на возможное решение проблемы
- auth_version = min(3, auth_max_version) # Пробуем более старую версию API
-
- # Выполняем авторизацию
- auth_url = f"{base_url}/{auth_path}"
- auth_params = {
- "api": "SYNO.API.Auth",
- "version": str(auth_version),
- "method": "login",
- "account": SYNOLOGY_USERNAME,
- "passwd": SYNOLOGY_PASSWORD,
- "session": "DirectHeaderTest",
- "format": "cookie"
- }
-
- if auth_version >= 6:
- auth_params["enable_syno_token"] = "yes"
-
- logger.info(f"Авторизация с использованием SYNO.API.Auth v{auth_version}")
- auth_response = session.get(auth_url, params=auth_params)
- auth_data = auth_response.json()
-
- if auth_data.get("success"):
- sid = auth_data.get("data", {}).get("sid")
- logger.info(f"Авторизация успешна! SID: {sid[:10]}...")
-
- # Теперь проверим, работает ли получение информации о системе
- # с настройкой cookie и заголовков
-
- # Сначала настроим куки для сохранения SID
- cookies = {
- 'id': sid,
- 'sid': sid
- }
- session.cookies.update(cookies)
-
- # Определяем путь для SYNO.DSM.Info
- info_info_url = f"{base_url}/entry.cgi"
- info_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.DSM.Info"
- }
-
- info_info_response = session.get(info_info_url, params=info_info_params)
- info_info_data = info_info_response.json()
-
- if info_info_data.get("success"):
- info_info = info_info_data.get("data", {}).get("SYNO.DSM.Info", {})
- info_path = info_info.get("path", "entry.cgi")
- info_max_version = info_info.get("maxVersion", 1)
- info_min_version = info_info.get("minVersion", 1)
-
- logger.info(f"API SYNO.DSM.Info: путь={info_path}, версия={info_min_version}-{info_max_version}")
-
- # Используем правильную версию API
- info_version = min(2, info_max_version)
-
- # Делаем запрос для получения информации о системе
- # с использованием sid как параметр запроса
- logger.info("Тест 2: Получение информации о системе с SID как параметром")
- info_url = f"{base_url}/{info_path}"
- info_params = {
- "api": "SYNO.DSM.Info",
- "version": str(info_version),
- "method": "getinfo",
- "_sid": sid
- }
-
- logger.info(f"Запрос информации с использованием SYNO.DSM.Info v{info_version}")
- info_response = session.get(info_url, params=info_params)
- info_data = info_response.json()
-
- if info_data.get("success"):
- logger.info("Успешно получена информация о системе!")
- logger.info(f"Данные: {json.dumps(info_data.get('data', {}), indent=2)}")
- else:
- error_code = info_data.get("error", {}).get("code", -1)
- logger.error(f"Не удалось получить информацию о системе. Ошибка: {error_code}")
-
- # Пробуем альтернативный способ
- logger.info("Тест 3: Попытка получить базовую информацию через SYNO.Core.System")
-
- # Определяем путь для SYNO.Core.System
- system_info_url = f"{base_url}/entry.cgi"
- system_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.Core.System"
- }
-
- system_info_response = session.get(system_info_url, params=system_info_params)
- system_info_data = system_info_response.json()
-
- if system_info_data.get("success"):
- system_info = system_info_data.get("data", {}).get("SYNO.Core.System", {})
- system_path = system_info.get("path", "entry.cgi")
- system_max_version = system_info.get("maxVersion", 1)
- system_min_version = system_info.get("minVersion", 1)
-
- logger.info(f"API SYNO.Core.System: путь={system_path}, версия={system_min_version}-{system_max_version}")
-
- # Используем правильную версию API
- system_version = 1
-
- # Пробуем альтернативную стратегию с X-SYNO-TOKEN
- # Некоторые API Synology требуют специальный токен в заголовках
- token = auth_data.get("data", {}).get("synotoken")
- if token:
- session.headers.update({'X-SYNO-TOKEN': token})
- logger.info(f"Добавлен X-SYNO-TOKEN: {token}")
-
- # Делаем запрос для получения информации о системе
- system_url = f"{base_url}/{system_path}"
- system_params = {
- "api": "SYNO.Core.System",
- "version": str(system_version),
- "method": "info",
- "_sid": sid
- }
-
- logger.info(f"Запрос информации с использованием SYNO.Core.System v{system_version}")
- system_response = session.get(system_url, params=system_params)
- system_data = system_response.json()
-
- if system_data.get("success"):
- logger.info("Успешно получена информация о системе через SYNO.Core.System!")
- logger.info(f"Данные: {json.dumps(system_data.get('data', {}), indent=2)}")
- else:
- error_code = system_data.get("error", {}).get("code", -1)
- logger.error(f"Не удалось получить информацию через SYNO.Core.System. Ошибка: {error_code}")
-
- # Пробуем другие методы для диагностики
- logger.info("Тест 4: Попытка использовать различные методы и заголовки")
-
- # Пробуем создать полностью новую сессию
- new_session = requests.Session()
- new_session.verify = False
- new_session.headers.update(custom_headers)
-
- # Пробуем версию 1 для авторизации
- auth_version = 1
- auth_params["version"] = str(auth_version)
-
- auth_response = new_session.get(auth_url, params=auth_params)
- auth_data = auth_response.json()
-
- if auth_data.get("success"):
- sid = auth_data.get("data", {}).get("sid")
- logger.info(f"Новая авторизация (v{auth_version}) успешна! SID: {sid[:10]}...")
-
- # Добавляем SID как куки
- cookies = {
- 'id': sid,
- 'sid': sid
- }
- new_session.cookies.update(cookies)
-
- # Пробуем другой подход: разделение запросов во времени
- logger.info("Тест 5: Разделение запросов во времени")
-
- # Даем некоторое время для инициализации сессии на сервере
- time.sleep(2)
-
- # Пробуем получить список файлов
- filestation_info_url = f"{base_url}/entry.cgi"
- filestation_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "SYNO.FileStation.List"
- }
-
- filestation_info_response = new_session.get(filestation_info_url, params=filestation_info_params)
- filestation_info_data = filestation_info_response.json()
-
- if filestation_info_data.get("success"):
- filestation_info = filestation_info_data.get("data", {}).get("SYNO.FileStation.List", {})
- filestation_path = filestation_info.get("path", "entry.cgi")
- filestation_max_version = filestation_info.get("maxVersion", 1)
-
- logger.info(f"API SYNO.FileStation.List: путь={filestation_path}, макс. версия={filestation_max_version}")
-
- # Используем правильную версию API
- filestation_version = min(2, filestation_max_version)
-
- # Делаем запрос для получения списка общих папок
- filestation_url = f"{base_url}/{filestation_path}"
- filestation_params = {
- "api": "SYNO.FileStation.List",
- "version": str(filestation_version),
- "method": "list_share",
- "_sid": sid
- }
-
- logger.info(f"Запрос списка общих папок с использованием SYNO.FileStation.List v{filestation_version}")
- filestation_response = new_session.get(filestation_url, params=filestation_params)
- filestation_data = filestation_response.json()
-
- if filestation_data.get("success"):
- logger.info("Успешно получен список общих папок!")
- shares = filestation_data.get("data", {}).get("shares", [])
- logger.info(f"Общие папки: {json.dumps(shares, indent=2)[:200]}...")
- else:
- error_code = filestation_data.get("error", {}).get("code", -1)
- logger.error(f"Не удалось получить список общих папок. Ошибка: {error_code}")
- else:
- error_code = auth_data.get("error", {}).get("code", -1)
- logger.error(f"Новая авторизация не удалась! Код ошибки: {error_code}")
- else:
- logger.error("Не удалось получить информацию о SYNO.Core.System API")
- else:
- logger.error("Не удалось получить информацию о SYNO.DSM.Info API")
- else:
- error_code = auth_data.get("error", {}).get("code", -1)
- logger.error(f"Авторизация не удалась! Код ошибки: {error_code}")
- else:
- logger.error("Не удалось получить информацию об API авторизации")
-
- except Exception as e:
- logger.error(f"Произошла ошибка: {str(e)}")
-
- # Тест 6: Проверка сетевой доступности
- logger.info("Тест 6: Проверка сетевой доступности")
-
- try:
- # Проверка базового TCP-соединения
- socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_obj.settimeout(SYNOLOGY_TIMEOUT)
- result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT))
- socket_obj.close()
-
- if result == 0:
- logger.info("TCP-соединение успешно установлено")
- else:
- logger.error(f"Не удалось установить TCP-соединение, код ошибки: {result}")
- except Exception as e:
- logger.error(f"Ошибка при проверке TCP-соединения: {str(e)}")
-
- # Тест 7: Запрос без аутентификации для проверки доступности API
- logger.info("Тест 7: Запрос без аутентификации для проверки доступности API")
-
- try:
- # Создаем новую сессию без аутентификации
- simple_session = requests.Session()
- simple_session.verify = False
-
- # Запрос к SYNO.API.Info не требует аутентификации
- api_info_url = f"{base_url}/entry.cgi"
- api_info_params = {
- "api": "SYNO.API.Info",
- "version": "1",
- "method": "query",
- "query": "all"
- }
-
- logger.info("Запрос информации о всех API без аутентификации")
- api_info_response = simple_session.get(api_info_url, params=api_info_params)
-
- if api_info_response.status_code == 200:
- logger.info("API доступно без аутентификации")
- api_info_data = api_info_response.json()
-
- if api_info_data.get("success"):
- logger.info("Успешно получена информация о всех API")
- api_count = len(api_info_data.get("data", {}))
- logger.info(f"Количество доступных API: {api_count}")
-
- # Поиск API для управления питанием
- power_apis = []
- for api_name, api_info in api_info_data.get("data", {}).items():
- if "power" in api_name.lower() or "reboot" in api_name.lower() or "shutdown" in api_name.lower():
- power_apis.append(f"{api_name}: {api_info}")
-
- logger.info(f"Найдены API для управления питанием: {power_apis}")
-
- # Поиск API для получения информации о системе
- info_apis = []
- for api_name, api_info in api_info_data.get("data", {}).items():
- if "info" in api_name.lower() or "system" in api_name.lower() or "status" in api_name.lower():
- info_apis.append(f"{api_name}: {api_info}")
-
- logger.info(f"Найдены API для информации о системе: {info_apis[:5]} и еще {len(info_apis)-5}")
- else:
- error_code = api_info_data.get("error", {}).get("code", -1)
- logger.error(f"Запрос к API без аутентификации не удался! Код ошибки: {error_code}")
- else:
- logger.error(f"API не доступно без аутентификации. HTTP статус: {api_info_response.status_code}")
- except Exception as e:
- logger.error(f"Ошибка при проверке доступности API: {str(e)}")
-
-if __name__ == "__main__":
- logger.info("Запуск теста API с заголовками")
- test_api_with_headers()
diff --git a/.history/test_reboot_20250830083539.py b/.history/test_reboot_20250830083539.py
deleted file mode 100644
index 90e896c..0000000
--- a/.history/test_reboot_20250830083539.py
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Скрипт для тестирования функции перезагрузки Synology NAS
-"""
-
-import os
-import sys
-import logging
-from pathlib import Path
-
-# Добавляем путь проекта в sys.path
-project_dir = str(Path(__file__).resolve().parent)
-if project_dir not in sys.path:
- sys.path.insert(0, project_dir)
-
-from src.api.synology import SynologyAPI
-from src.config.config import SYNOLOGY_POWER_API, SYNOLOGY_INFO_API, SYNOLOGY_API_VERSION
-
-# Настройка логирования
-logging.basicConfig(
- level=logging.DEBUG,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-def main():
- """Основная функция для тестирования перезагрузки"""
- logger.info("Тестирование функции перезагрузки Synology NAS")
-
- logger.info(f"Используемые API: POWER={SYNOLOGY_POWER_API}, INFO={SYNOLOGY_INFO_API}, VERSION={SYNOLOGY_API_VERSION}")
-
- # Инициализация API
- synology = SynologyAPI()
-
- # Проверка онлайн статуса
- logger.info("Проверка онлайн статуса...")
- is_online = synology.is_online(force_check=True)
- logger.info(f"Статус: {'Онлайн' if is_online else 'Оффлайн'}")
-
- if not is_online:
- logger.error("NAS недоступен. Невозможно выполнить перезагрузку.")
- return
-
- # Вывод информации о системе
- logger.info("Получение информации о системе...")
- system_info = synology.get_system_status()
-
- if system_info.get("status") == "error":
- logger.error(f"Ошибка получения информации о системе: {system_info.get('error')}")
- else:
- logger.info(f"Информация о системе: {system_info}")
-
- # Запрос на подтверждение действия
- confirm = input("Вы действительно хотите перезагрузить Synology NAS? (y/n): ")
- if confirm.lower() != 'y':
- logger.info("Операция отменена пользователем.")
- return
-
- # Выполнение перезагрузки
- logger.info("Выполнение перезагрузки...")
- try:
- result = synology.reboot_system()
- if result:
- logger.info("Перезагрузка выполнена успешно.")
- else:
- logger.error("Не удалось выполнить перезагрузку.")
- except Exception as e:
- logger.error(f"Ошибка при выполнении перезагрузки: {str(e)}")
-
-if __name__ == "__main__":
- main()
diff --git a/.history/test_reboot_20250830083624.py b/.history/test_reboot_20250830083624.py
deleted file mode 100644
index 90e896c..0000000
--- a/.history/test_reboot_20250830083624.py
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Скрипт для тестирования функции перезагрузки Synology NAS
-"""
-
-import os
-import sys
-import logging
-from pathlib import Path
-
-# Добавляем путь проекта в sys.path
-project_dir = str(Path(__file__).resolve().parent)
-if project_dir not in sys.path:
- sys.path.insert(0, project_dir)
-
-from src.api.synology import SynologyAPI
-from src.config.config import SYNOLOGY_POWER_API, SYNOLOGY_INFO_API, SYNOLOGY_API_VERSION
-
-# Настройка логирования
-logging.basicConfig(
- level=logging.DEBUG,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-def main():
- """Основная функция для тестирования перезагрузки"""
- logger.info("Тестирование функции перезагрузки Synology NAS")
-
- logger.info(f"Используемые API: POWER={SYNOLOGY_POWER_API}, INFO={SYNOLOGY_INFO_API}, VERSION={SYNOLOGY_API_VERSION}")
-
- # Инициализация API
- synology = SynologyAPI()
-
- # Проверка онлайн статуса
- logger.info("Проверка онлайн статуса...")
- is_online = synology.is_online(force_check=True)
- logger.info(f"Статус: {'Онлайн' if is_online else 'Оффлайн'}")
-
- if not is_online:
- logger.error("NAS недоступен. Невозможно выполнить перезагрузку.")
- return
-
- # Вывод информации о системе
- logger.info("Получение информации о системе...")
- system_info = synology.get_system_status()
-
- if system_info.get("status") == "error":
- logger.error(f"Ошибка получения информации о системе: {system_info.get('error')}")
- else:
- logger.info(f"Информация о системе: {system_info}")
-
- # Запрос на подтверждение действия
- confirm = input("Вы действительно хотите перезагрузить Synology NAS? (y/n): ")
- if confirm.lower() != 'y':
- logger.info("Операция отменена пользователем.")
- return
-
- # Выполнение перезагрузки
- logger.info("Выполнение перезагрузки...")
- try:
- result = synology.reboot_system()
- if result:
- logger.info("Перезагрузка выполнена успешно.")
- else:
- logger.error("Не удалось выполнить перезагрузку.")
- except Exception as e:
- logger.error(f"Ошибка при выполнении перезагрузки: {str(e)}")
-
-if __name__ == "__main__":
- main()
diff --git a/.history/test_system_info_20250830083606.py b/.history/test_system_info_20250830083606.py
deleted file mode 100644
index 95a9bf3..0000000
--- a/.history/test_system_info_20250830083606.py
+++ /dev/null
@@ -1,87 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Скрипт для тестирования получения информации о системе Synology NAS
-"""
-
-import os
-import sys
-import logging
-from pathlib import Path
-
-# Добавляем путь проекта в sys.path
-project_dir = str(Path(__file__).resolve().parent)
-if project_dir not in sys.path:
- sys.path.insert(0, project_dir)
-
-from src.api.synology import SynologyAPI
-from src.config.config import SYNOLOGY_POWER_API, SYNOLOGY_INFO_API, SYNOLOGY_API_VERSION
-
-# Настройка логирования
-logging.basicConfig(
- level=logging.DEBUG,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-def main():
- """Основная функция для тестирования получения информации о системе"""
- logger.info("Тестирование получения информации о системе Synology NAS")
-
- logger.info(f"Используемые API: POWER={SYNOLOGY_POWER_API}, INFO={SYNOLOGY_INFO_API}, VERSION={SYNOLOGY_API_VERSION}")
-
- # Инициализация API
- synology = SynologyAPI()
-
- # Проверка онлайн статуса
- logger.info("Проверка онлайн статуса...")
- is_online = synology.is_online(force_check=True)
- logger.info(f"Статус: {'Онлайн' if is_online else 'Оффлайн'}")
-
- if not is_online:
- logger.error("NAS недоступен. Невозможно получить информацию о системе.")
- return
-
- # Получение списка доступных API
- logger.info("Получение списка доступных API...")
- from src.api.api_discovery import discover_available_apis
- from src.config.config import SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_SECURE
-
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- apis = discover_available_apis(base_url)
- if apis:
- logger.info(f"Найдено {len(apis)} API")
-
- # Фильтрация API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_apis = [name for name in apis.keys() if "system" in name.lower() or "dsm.info" in name.lower()]
-
- logger.info(f"API для управления питанием: {power_apis}")
- logger.info(f"API для системной информации: {system_apis}")
-
- # Проверка конкретных API
- for api_name in [SYNOLOGY_POWER_API, SYNOLOGY_INFO_API]:
- if api_name in apis:
- api_info = apis[api_name]
- logger.info(f"API {api_name}: versions={api_info.get('minVersion')}-{api_info.get('maxVersion')}, path={api_info.get('path')}")
- else:
- logger.warning(f"API {api_name} не найден в списке доступных API")
- else:
- logger.error("Не удалось получить список доступных API")
-
- # Вывод информации о системе
- logger.info("Получение информации о системе...")
- system_info = synology.get_system_status()
-
- if system_info.get("status") == "error":
- logger.error(f"Ошибка получения информации о системе: {system_info.get('error')}")
- else:
- logger.info(f"Информация о системе: {system_info}")
-
- logger.info("Тестирование завершено.")
-
-if __name__ == "__main__":
- main()
diff --git a/.history/test_system_info_20250830083624.py b/.history/test_system_info_20250830083624.py
deleted file mode 100644
index 95a9bf3..0000000
--- a/.history/test_system_info_20250830083624.py
+++ /dev/null
@@ -1,87 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Скрипт для тестирования получения информации о системе Synology NAS
-"""
-
-import os
-import sys
-import logging
-from pathlib import Path
-
-# Добавляем путь проекта в sys.path
-project_dir = str(Path(__file__).resolve().parent)
-if project_dir not in sys.path:
- sys.path.insert(0, project_dir)
-
-from src.api.synology import SynologyAPI
-from src.config.config import SYNOLOGY_POWER_API, SYNOLOGY_INFO_API, SYNOLOGY_API_VERSION
-
-# Настройка логирования
-logging.basicConfig(
- level=logging.DEBUG,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-logger = logging.getLogger(__name__)
-
-def main():
- """Основная функция для тестирования получения информации о системе"""
- logger.info("Тестирование получения информации о системе Synology NAS")
-
- logger.info(f"Используемые API: POWER={SYNOLOGY_POWER_API}, INFO={SYNOLOGY_INFO_API}, VERSION={SYNOLOGY_API_VERSION}")
-
- # Инициализация API
- synology = SynologyAPI()
-
- # Проверка онлайн статуса
- logger.info("Проверка онлайн статуса...")
- is_online = synology.is_online(force_check=True)
- logger.info(f"Статус: {'Онлайн' if is_online else 'Оффлайн'}")
-
- if not is_online:
- logger.error("NAS недоступен. Невозможно получить информацию о системе.")
- return
-
- # Получение списка доступных API
- logger.info("Получение списка доступных API...")
- from src.api.api_discovery import discover_available_apis
- from src.config.config import SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_SECURE
-
- protocol = "https" if SYNOLOGY_SECURE else "http"
- base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi"
-
- apis = discover_available_apis(base_url)
- if apis:
- logger.info(f"Найдено {len(apis)} API")
-
- # Фильтрация API для управления питанием
- power_apis = [name for name in apis.keys() if "power" in name.lower()]
- system_apis = [name for name in apis.keys() if "system" in name.lower() or "dsm.info" in name.lower()]
-
- logger.info(f"API для управления питанием: {power_apis}")
- logger.info(f"API для системной информации: {system_apis}")
-
- # Проверка конкретных API
- for api_name in [SYNOLOGY_POWER_API, SYNOLOGY_INFO_API]:
- if api_name in apis:
- api_info = apis[api_name]
- logger.info(f"API {api_name}: versions={api_info.get('minVersion')}-{api_info.get('maxVersion')}, path={api_info.get('path')}")
- else:
- logger.warning(f"API {api_name} не найден в списке доступных API")
- else:
- logger.error("Не удалось получить список доступных API")
-
- # Вывод информации о системе
- logger.info("Получение информации о системе...")
- system_info = synology.get_system_status()
-
- if system_info.get("status") == "error":
- logger.error(f"Ошибка получения информации о системе: {system_info.get('error')}")
- else:
- logger.info(f"Информация о системе: {system_info}")
-
- logger.info("Тестирование завершено.")
-
-if __name__ == "__main__":
- main()
diff --git a/.history/ОТЧЕТ_ПО_API_20250830090431.md b/.history/ОТЧЕТ_ПО_API_20250830090431.md
deleted file mode 100644
index 68eba65..0000000
--- a/.history/ОТЧЕТ_ПО_API_20250830090431.md
+++ /dev/null
@@ -1,185 +0,0 @@
-# Отчет по доступным API и возможностям управления Synology NAS
-
-## 1. Доступные API Synology NAS
-
-На вашем Synology NAS (модель DS223j с DSM 7.2.2) обнаружено **572** различных API. Ниже перечислены ключевые категории API, которые можно использовать для расширения функциональности бота:
-
-### 1.1. API для управления питанием
-- **SYNO.Core.Hardware.PowerRecovery** (v1) - основной API для управления питанием
-- **SYNO.Core.Hardware.PowerSchedule** (v1) - API для настройки расписания включения/выключения
-- **SYNO.Core.Hardware.NeedReboot** (v1) - API для перезагрузки системы
-
-### 1.2. API для системной информации
-- **SYNO.DSM.Info** (v2) - основной API для получения общей информации о системе
-- **SYNO.Core.System** (v1) - API для получения расширенной системной информации
-- **SYNO.Core.System.Status** (v1) - API для получения статуса системы
-- **SYNO.Core.System.Utilization** (v1) - API для получения сведений о загрузке системы
-
-### 1.3. API для хранилища и файлов
-- **SYNO.Storage.CGI.Storage** - информация о хранилище и дисках
-- **SYNO.FileStation.List** - получение списка файлов и папок
-
-### 1.4. API для мониторинга
-- **SYNO.Core.System.Process** - информация о запущенных процессах
-- **SYNO.Core.System.SystemHealth** - состояние здоровья системы
-
-## 2. Расширенные команды управления NAS
-
-Бот уже поддерживает следующие базовые команды:
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS
-- `/help` - Вывод справки
-
-Также реализованы расширенные команды:
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-- `/checkapi` - Проверка доступных API Synology
-
-### 2.1. Рекомендуемые дополнительные команды
-
-На основе анализа доступных API предлагаю добавить следующие команды:
-
-#### 2.1.1. Управление питанием и расписанием
-- `/schedule` - Управление расписанием включения/выключения NAS
-- `/wakeup` - Немедленное включение NAS через Wake-on-LAN
-- `/quickreboot` - Быстрая перезагрузка без запроса подтверждения
-
-#### 2.1.2. Мониторинг системы
-- `/processes` - Просмотр активных процессов и их загрузки
-- `/network` - Детальная информация о сетевых подключениях
-- `/temperature` - Мониторинг температуры системы и дисков
-- `/updates` - Проверка доступных обновлений для DSM
-
-#### 2.1.3. Управление файлами
-- `/browse [путь]` - Просмотр файлов в указанной директории
-- `/search [шаблон]` - Поиск файлов по шаблону
-- `/quota` - Просмотр информации о квотах пользователей
-
-#### 2.1.4. Резервное копирование
-- `/backup` - Управление задачами резервного копирования
-- `/backupstatus` - Проверка статуса резервного копирования
-
-## 3. Возможности использования API
-
-### 3.1. Оптимизация текущего кода
-- Обнаружено, что для успешного взаимодействия с API необходимы специальные HTTP-заголовки, имитирующие браузер
-- API версии 3 показывает лучшую стабильность для базовых операций, чем версия 6
-- Для аутентификации рекомендуется использовать куки и форматы, совместимые с веб-интерфейсом
-
-### 3.2. Рекомендуемые настройки API
-- **SYNOLOGY_POWER_API = SYNO.Core.Hardware.PowerRecovery**
-- **SYNOLOGY_INFO_API = SYNO.DSM.Info**
-- **SYNOLOGY_API_VERSION = 2** (вместо текущего значения 6)
-
-### 3.3. Новые функциональные возможности
-
-#### 3.3.1. Мониторинг производительности
-```python
-def get_performance_stats():
- """Получение детальной статистики производительности"""
- result = api._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
- return result
-```
-
-#### 3.3.2. Управление сервисами
-```python
-def manage_services(service_name, action="status"):
- """Управление системными сервисами (start/stop/restart/status)"""
- result = api._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
- return result
-```
-
-#### 3.3.3. Просмотр журналов
-```python
-def get_system_logs(limit=20):
- """Получение системных журналов"""
- result = api._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"limit": limit})
- return result
-```
-
-#### 3.3.4. Настройка расписания питания
-```python
-def set_power_schedule(days, time, action="boot"):
- """Настройка расписания питания (boot/shutdown)"""
- result = api._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1,
- params={"day": days, "time": time, "action": action})
- return result
-```
-
-## 4. Решение текущих проблем API
-
-### 4.1. Проблема с получением информации о хранилище
-Текущий код использует заглушку для `get_storage_status()`. Рекомендуемая реализация:
-
-```python
-def get_storage_status(self) -> Dict[str, Any]:
- """Получение информации о хранилище"""
- # Попробуем получить информацию о дисках
- disk_result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not disk_result:
- # Пробуем альтернативный API
- disk_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- # Собираем результат
- volumes = disk_result.get("volumes", [])
- disks = disk_result.get("disks", [])
-
- total_size = sum(vol.get("size", {}).get("total", 0) for vol in volumes)
- total_used = sum(vol.get("size", {}).get("used", 0) for vol in volumes)
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-```
-
-### 4.2. Проблема с получением списка общих папок
-
-```python
-def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- return []
-
- return result.get("shares", [])
-```
-
-### 4.3. Проблема с получением информации о нагрузке
-
-```python
-def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о нагрузке системы"""
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- return {}
-
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-```
-
-## 5. Заключение
-
-Synology NAS предоставляет обширный набор API, которые можно использовать для создания полноценного Telegram-бота для управления и мониторинга. Основные рекомендации:
-
-1. Обновить версию API до более стабильной (v2-3 вместо v6)
-2. Добавить специальные HTTP-заголовки для имитации веб-браузера
-3. Использовать куки для сохранения сессии
-4. Реализовать новые функции управления на основе доступных API
-5. Добавить обработку ошибок для нестабильных API (особенно хранилища и общих папок)
-
-Дополнительно рекомендуется реализовать функции автоматического мониторинга и уведомления о важных событиях, таких как высокая температура, заканчивающееся место на дисках или необходимость обновления системы.
diff --git a/.history/ОТЧЕТ_ПО_API_20250830090511.md b/.history/ОТЧЕТ_ПО_API_20250830090511.md
deleted file mode 100644
index 68eba65..0000000
--- a/.history/ОТЧЕТ_ПО_API_20250830090511.md
+++ /dev/null
@@ -1,185 +0,0 @@
-# Отчет по доступным API и возможностям управления Synology NAS
-
-## 1. Доступные API Synology NAS
-
-На вашем Synology NAS (модель DS223j с DSM 7.2.2) обнаружено **572** различных API. Ниже перечислены ключевые категории API, которые можно использовать для расширения функциональности бота:
-
-### 1.1. API для управления питанием
-- **SYNO.Core.Hardware.PowerRecovery** (v1) - основной API для управления питанием
-- **SYNO.Core.Hardware.PowerSchedule** (v1) - API для настройки расписания включения/выключения
-- **SYNO.Core.Hardware.NeedReboot** (v1) - API для перезагрузки системы
-
-### 1.2. API для системной информации
-- **SYNO.DSM.Info** (v2) - основной API для получения общей информации о системе
-- **SYNO.Core.System** (v1) - API для получения расширенной системной информации
-- **SYNO.Core.System.Status** (v1) - API для получения статуса системы
-- **SYNO.Core.System.Utilization** (v1) - API для получения сведений о загрузке системы
-
-### 1.3. API для хранилища и файлов
-- **SYNO.Storage.CGI.Storage** - информация о хранилище и дисках
-- **SYNO.FileStation.List** - получение списка файлов и папок
-
-### 1.4. API для мониторинга
-- **SYNO.Core.System.Process** - информация о запущенных процессах
-- **SYNO.Core.System.SystemHealth** - состояние здоровья системы
-
-## 2. Расширенные команды управления NAS
-
-Бот уже поддерживает следующие базовые команды:
-- `/start` - Начало работы с ботом
-- `/status` - Проверка текущего статуса NAS
-- `/power` - Управление питанием NAS
-- `/help` - Вывод справки
-
-Также реализованы расширенные команды:
-- `/system` - Подробная информация о системе
-- `/storage` - Информация о хранилище и дисках
-- `/shares` - Список общих папок
-- `/load` - Текущая нагрузка на систему
-- `/security` - Статус безопасности системы
-- `/checkapi` - Проверка доступных API Synology
-
-### 2.1. Рекомендуемые дополнительные команды
-
-На основе анализа доступных API предлагаю добавить следующие команды:
-
-#### 2.1.1. Управление питанием и расписанием
-- `/schedule` - Управление расписанием включения/выключения NAS
-- `/wakeup` - Немедленное включение NAS через Wake-on-LAN
-- `/quickreboot` - Быстрая перезагрузка без запроса подтверждения
-
-#### 2.1.2. Мониторинг системы
-- `/processes` - Просмотр активных процессов и их загрузки
-- `/network` - Детальная информация о сетевых подключениях
-- `/temperature` - Мониторинг температуры системы и дисков
-- `/updates` - Проверка доступных обновлений для DSM
-
-#### 2.1.3. Управление файлами
-- `/browse [путь]` - Просмотр файлов в указанной директории
-- `/search [шаблон]` - Поиск файлов по шаблону
-- `/quota` - Просмотр информации о квотах пользователей
-
-#### 2.1.4. Резервное копирование
-- `/backup` - Управление задачами резервного копирования
-- `/backupstatus` - Проверка статуса резервного копирования
-
-## 3. Возможности использования API
-
-### 3.1. Оптимизация текущего кода
-- Обнаружено, что для успешного взаимодействия с API необходимы специальные HTTP-заголовки, имитирующие браузер
-- API версии 3 показывает лучшую стабильность для базовых операций, чем версия 6
-- Для аутентификации рекомендуется использовать куки и форматы, совместимые с веб-интерфейсом
-
-### 3.2. Рекомендуемые настройки API
-- **SYNOLOGY_POWER_API = SYNO.Core.Hardware.PowerRecovery**
-- **SYNOLOGY_INFO_API = SYNO.DSM.Info**
-- **SYNOLOGY_API_VERSION = 2** (вместо текущего значения 6)
-
-### 3.3. Новые функциональные возможности
-
-#### 3.3.1. Мониторинг производительности
-```python
-def get_performance_stats():
- """Получение детальной статистики производительности"""
- result = api._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
- return result
-```
-
-#### 3.3.2. Управление сервисами
-```python
-def manage_services(service_name, action="status"):
- """Управление системными сервисами (start/stop/restart/status)"""
- result = api._make_api_request("SYNO.Core.Service", action, version=1,
- params={"service": service_name})
- return result
-```
-
-#### 3.3.3. Просмотр журналов
-```python
-def get_system_logs(limit=20):
- """Получение системных журналов"""
- result = api._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1,
- params={"limit": limit})
- return result
-```
-
-#### 3.3.4. Настройка расписания питания
-```python
-def set_power_schedule(days, time, action="boot"):
- """Настройка расписания питания (boot/shutdown)"""
- result = api._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1,
- params={"day": days, "time": time, "action": action})
- return result
-```
-
-## 4. Решение текущих проблем API
-
-### 4.1. Проблема с получением информации о хранилище
-Текущий код использует заглушку для `get_storage_status()`. Рекомендуемая реализация:
-
-```python
-def get_storage_status(self) -> Dict[str, Any]:
- """Получение информации о хранилище"""
- # Попробуем получить информацию о дисках
- disk_result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1)
-
- if not disk_result:
- # Пробуем альтернативный API
- disk_result = self._make_api_request("SYNO.Core.Storage", "info", version=1)
-
- # Собираем результат
- volumes = disk_result.get("volumes", [])
- disks = disk_result.get("disks", [])
-
- total_size = sum(vol.get("size", {}).get("total", 0) for vol in volumes)
- total_used = sum(vol.get("size", {}).get("used", 0) for vol in volumes)
-
- return {
- "volumes": volumes,
- "disks": disks,
- "total_size": total_size,
- "total_used": total_used
- }
-```
-
-### 4.2. Проблема с получением списка общих папок
-
-```python
-def get_shared_folders(self) -> List[Dict[str, Any]]:
- """Получение списка общих папок"""
- result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2)
-
- if not result:
- return []
-
- return result.get("shares", [])
-```
-
-### 4.3. Проблема с получением информации о нагрузке
-
-```python
-def get_system_load(self) -> Dict[str, Any]:
- """Получение информации о нагрузке системы"""
- result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1)
-
- if not result:
- return {}
-
- return {
- "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0),
- "memory": result.get("memory", {}),
- "network": result.get("network", {})
- }
-```
-
-## 5. Заключение
-
-Synology NAS предоставляет обширный набор API, которые можно использовать для создания полноценного Telegram-бота для управления и мониторинга. Основные рекомендации:
-
-1. Обновить версию API до более стабильной (v2-3 вместо v6)
-2. Добавить специальные HTTP-заголовки для имитации веб-браузера
-3. Использовать куки для сохранения сессии
-4. Реализовать новые функции управления на основе доступных API
-5. Добавить обработку ошибок для нестабильных API (особенно хранилища и общих папок)
-
-Дополнительно рекомендуется реализовать функции автоматического мониторинга и уведомления о важных событиях, таких как высокая температура, заканчивающееся место на дисках или необходимость обновления системы.