From b1ba48e9bc1cf3a80d2864f6754a016ff224c43a Mon Sep 17 00:00:00 2001 From: "Choi A.K." Date: Sat, 30 Aug 2025 14:48:04 +0900 Subject: [PATCH] Files cleaning --- .env.example | 0 .gitignore | 2 +- .history/.dockerignore_20250830102713 | 58 - .history/.dockerignore_20250830103154 | 58 - .history/.dockerignore_20250830103255 | 59 - .history/.env-example_20250830103005 | 26 - .history/.env-example_20250830103154 | 26 - .history/.env_20250830063713 | 15 - .history/.env_20250830063839 | 15 - .history/.env_20250830071119 | 15 - .history/.env_20250830071139 | 15 - .history/.env_20250830071153 | 15 - .history/.env_20250830071249 | 15 - .history/.env_20250830071300 | 15 - .history/.env_20250830072439.example | 16 - .history/.env_20250830072817.example | 16 - .history/.env_20250830080745 | 16 - .history/.env_20250830082200 | 16 - .history/.env_20250830082210 | 20 - .history/.env_20250830082500 | 20 - .history/.env_20250830083237 | 20 - .history/.env_20250830101005 | 20 - .history/.env_20250830101843 | 20 - .history/.gitignore_20250830063748 | 44 - .history/.gitignore_20250830063839 | 44 - .history/DOCKER_DEPLOYMENT_20250830103243.md | 218 -- .history/DOCKER_DEPLOYMENT_20250830103340.md | 218 -- .history/Dockerfile_20250830102631 | 20 - .history/Dockerfile_20250830102802 | 23 - .history/Dockerfile_20250830103154 | 23 - .history/README_20250830063733.md | 101 - .history/README_20250830063839.md | 101 - .history/README_20250830065402.md | 117 - .history/README_20250830065412.md | 125 -- .history/README_20250830065454.md | 125 -- .history/README_20250830072408.md | 126 -- .history/README_20250830072817.md | 126 -- .history/README_20250830092253.md | 128 -- .history/README_20250830092310.md | 131 -- .history/README_20250830092330.md | 146 -- .history/README_20250830092350.md | 151 -- .history/README_20250830092440.md | 151 -- .history/README_20250830103059.md | 192 -- .history/README_20250830103119.md | 201 -- .history/README_20250830103139.md | 236 -- .history/README_20250830103154.md | 236 -- .history/README_DOCKER_20250830102736.md | 106 - .history/README_DOCKER_20250830103154.md | 106 - .history/deploy_20250830102934.sh | 49 - .history/deploy_20250830102949.cmd | 45 - .history/deploy_20250830103154.cmd | 45 - .history/deploy_20250830103154.sh | 49 - .history/diagnose_api_20250830081925.py | 91 - .history/diagnose_api_20250830081957.py | 91 - .history/direct_api_test_20250830084231.py | 293 --- .history/direct_api_test_20250830084257.py | 293 --- .history/docker-compose_20250830102643.yml | 21 - .history/docker-compose_20250830102820.yml | 36 - .history/docker-compose_20250830102916.yml | 38 - .history/docker-compose_20250830103154.yml | 38 - .../docs/file_manager_agent_20250830141933.md | 61 - .../docs/file_manager_agent_20250830141957.md | 61 - .history/entrypoint_20250830102747.sh | 7 - .history/entrypoint_20250830103154.sh | 7 - .../file_manager_demo_20250830141907.py | 52 - .../file_manager_demo_20250830141957.py | 52 - .history/requirements_20250830063740.txt | 4 - .history/requirements_20250830063839.txt | 4 - .history/requirements_20250830065002.txt | 5 - .history/requirements_20250830065455.txt | 5 - .history/requirements_20250830072350.txt | 4 - .history/requirements_20250830072817.txt | 4 - .history/requirements_20250830092418.txt | 6 - .history/requirements_20250830092441.txt | 6 - .history/run_20250830063754.py | 11 - .history/run_20250830063839.py | 11 - .history/run_20250830102904.py | 18 - .history/run_20250830103154.py | 18 - .history/run_bot_20250830082521.py | 11 - .history/run_bot_20250830082536.py | 11 - .history/run_bot_20250830142127.py | 13 - .history/run_bot_20250830142131.py | 11 - .../src/agents/__init___20250830141428.py | 9 - .../src/agents/__init___20250830141957.py | 9 - .../file_manager_agent_20250830141230.py | 653 ------ .../file_manager_agent_20250830141546.py | 653 ------ .../file_manager_agent_20250830141613.py | 693 ------ .../file_manager_agent_20250830141646.py | 743 ------- .../file_manager_agent_20250830141721.py | 750 ------- .../file_manager_agent_20250830141747.py | 750 ------- .../file_manager_agent_20250830141805.py | 751 ------- .../file_manager_agent_20250830141832.py | 756 ------- .../file_manager_agent_20250830141847.py | 757 ------- .../file_manager_agent_20250830141957.py | 757 ------- .../file_manager_agent_20250830142055.py | 757 ------- .../file_manager_agent_20250830142117.py | 757 ------- .../file_manager_agent_20250830142754.py | 760 ------- .../file_manager_agent_20250830142812.py | 763 ------- .../file_manager_agent_20250830142848.py | 766 ------- .../file_manager_agent_20250830142901.py | 769 ------- .../file_manager_agent_20250830142941.py | 775 ------- .../file_manager_agent_20250830143005.py | 775 ------- .../file_manager_agent_20250830143049.py | 775 ------- .../file_manager_agent_20250830143114.py | 785 ------- .../file_manager_agent_20250830143155.py | 785 ------- .../file_manager_agent_20250830143317.py | 784 ------- .../file_manager_agent_20250830143333.py | 789 ------- .../file_manager_agent_20250830143351.py | 794 ------- .../file_manager_agent_20250830143422.py | 812 ------- .../file_manager_agent_20250830143501.py | 817 ------- .../file_manager_agent_20250830143531.py | 828 ------- .../file_manager_agent_20250830143559.py | 834 ------- .../file_manager_agent_20250830143628.py | 844 ------- .../file_manager_agent_20250830143646.py | 844 ------- .../src/api/api_discovery_20250830081819.py | 113 - .../src/api/api_discovery_20250830081957.py | 113 - .../api_version_resolver_20250830084129.py | 234 -- .../api_version_resolver_20250830084257.py | 234 -- .../src/api/filestation_20250830141415.py | 512 ----- .../src/api/filestation_20250830141957.py | 512 ----- .history/src/api/synology_20250830063552.py | 262 --- .history/src/api/synology_20250830063839.py | 262 --- .history/src/api/synology_20250830065021.py | 267 --- .history/src/api/synology_20250830065110.py | 315 --- .history/src/api/synology_20250830065154.py | 447 ---- .history/src/api/synology_20250830065454.py | 447 ---- .history/src/api/synology_20250830071505.py | 447 ---- .history/src/api/synology_20250830071525.py | 447 ---- .history/src/api/synology_20250830071727.py | 446 ---- .history/src/api/synology_20250830071755.py | 446 ---- .history/src/api/synology_20250830071934.py | 445 ---- .history/src/api/synology_20250830072031.py | 398 ---- .history/src/api/synology_20250830072122.py | 287 --- .history/src/api/synology_20250830072817.py | 287 --- .history/src/api/synology_20250830073007.py | 309 --- .history/src/api/synology_20250830073043.py | 309 --- .history/src/api/synology_20250830073131.py | 326 --- .history/src/api/synology_20250830073142.py | 326 --- .history/src/api/synology_20250830073153.py | 326 --- .history/src/api/synology_20250830073204.py | 326 --- .history/src/api/synology_20250830073217.py | 326 --- .history/src/api/synology_20250830073544.py | 353 --- .history/src/api/synology_20250830073606.py | 380 ---- .history/src/api/synology_20250830073620.py | 380 ---- .history/src/api/synology_20250830073939.py | 432 ---- .history/src/api/synology_20250830073954.py | 432 ---- .history/src/api/synology_20250830074025.py | 458 ---- .history/src/api/synology_20250830074140.py | 458 ---- .history/src/api/synology_20250830074228.py | 479 ---- .history/src/api/synology_20250830074245.py | 482 ---- .history/src/api/synology_20250830074313.py | 482 ---- .history/src/api/synology_20250830074442.py | 482 ---- .history/src/api/synology_20250830074627.py | 499 ----- .history/src/api/synology_20250830074636.py | 500 ----- .history/src/api/synology_20250830074708.py | 562 ----- .history/src/api/synology_20250830074721.py | 562 ----- .history/src/api/synology_20250830074730.py | 561 ----- .history/src/api/synology_20250830074755.py | 637 ------ .history/src/api/synology_20250830074838.py | 686 ------ .history/src/api/synology_20250830074850.py | 686 ------ .history/src/api/synology_20250830074920.py | 656 ------ .history/src/api/synology_20250830074947.py | 669 ------ .history/src/api/synology_20250830075007.py | 717 ------ .history/src/api/synology_20250830075041.py | 762 ------- .history/src/api/synology_20250830075053.py | 761 ------- .history/src/api/synology_20250830075107.py | 761 ------- .history/src/api/synology_20250830075248.py | 761 ------- .history/src/api/synology_20250830075326.py | 762 ------- .history/src/api/synology_20250830075348.py | 762 ------- .history/src/api/synology_20250830075503.py | 770 ------- .history/src/api/synology_20250830075522.py | 770 ------- .history/src/api/synology_20250830080635.py | 769 ------- .history/src/api/synology_20250830080658.py | 769 ------- .history/src/api/synology_20250830080742.py | 819 ------- .history/src/api/synology_20250830080825.py | 866 -------- .history/src/api/synology_20250830080858.py | 866 -------- .history/src/api/synology_20250830081426.py | 910 -------- .history/src/api/synology_20250830081505.py | 943 -------- .history/src/api/synology_20250830081538.py | 956 -------- .history/src/api/synology_20250830081615.py | 956 -------- .history/src/api/synology_20250830081654.py | 972 -------- .history/src/api/synology_20250830081744.py | 976 -------- .history/src/api/synology_20250830081837.py | 978 -------- .history/src/api/synology_20250830081856.py | 977 -------- .history/src/api/synology_20250830081957.py | 977 -------- .history/src/api/synology_20250830082235.py | 980 -------- .history/src/api/synology_20250830082307.py | 1018 --------- .history/src/api/synology_20250830082353.py | 1048 --------- .history/src/api/synology_20250830082444.py | 1097 --------- .history/src/api/synology_20250830082500.py | 1097 --------- .history/src/api/synology_20250830082853.py | 1150 ---------- .history/src/api/synology_20250830082954.py | 1189 ---------- .history/src/api/synology_20250830083115.py | 1189 ---------- .history/src/api/synology_20250830084539.py | 1204 ---------- .history/src/api/synology_20250830084644.py | 1218 ---------- .history/src/api/synology_20250830084803.py | 1218 ---------- .history/src/api/synology_20250830090902.py | 1317 ----------- .history/src/api/synology_20250830090936.py | 1364 ------------ .history/src/api/synology_20250830091024.py | 1480 ------------- .history/src/api/synology_20250830091124.py | 1773 --------------- .history/src/api/synology_20250830091218.py | 1903 ---------------- .history/src/api/synology_20250830092441.py | 1903 ---------------- .history/src/api/synology_20250830095113.py | 1907 ---------------- .history/src/api/synology_20250830095635.py | 1918 ---------------- .history/src/api/synology_20250830095651.py | 1918 ---------------- .history/src/api/synology_20250830100555.py | 1944 ---------------- .history/src/api/synology_20250830101023.py | 1948 ---------------- .history/src/api/synology_20250830101047.py | 1961 ----------------- .history/src/api/synology_20250830101104.py | 1954 ---------------- .history/src/api/synology_20250830101145.py | 1944 ---------------- .history/src/api/synology_20250830101211.py | 1895 ---------------- .history/src/api/synology_20250830101236.py | 1861 ---------------- .history/src/api/synology_20250830101843.py | 1861 ---------------- .history/src/api/synology_20250830104812.py | 1873 ---------------- .history/src/api/synology_20250830104833.py | 1877 ---------------- .history/src/api/synology_20250830104945.py | 1877 ---------------- .history/src/api/synology_20250830105105.py | 1894 ---------------- .history/src/api/synology_20250830105130.py | 1908 ---------------- .history/src/api/synology_20250830110338.py | 1908 ---------------- .history/src/bot_20250830063649.py | 57 - .history/src/bot_20250830063839.py | 57 - .history/src/bot_20250830065301.py | 64 - .history/src/bot_20250830065311.py | 71 - .history/src/bot_20250830065454.py | 71 - .history/src/bot_20250830072835.py | 71 - .history/src/bot_20250830072844.py | 71 - .history/src/bot_20250830075657.py | 74 - .history/src/bot_20250830075723.py | 102 - .history/src/bot_20250830075740.py | 102 - .history/src/bot_20250830075757.py | 102 - .history/src/bot_20250830083325.py | 103 - .history/src/bot_20250830083341.py | 104 - .history/src/bot_20250830083502.py | 104 - .history/src/bot_20250830091533.py | 119 - .history/src/bot_20250830091644.py | 134 -- .history/src/bot_20250830092152.py | 136 -- .history/src/bot_20250830092440.py | 136 -- .history/src/bot_20250830093455.py | 139 -- .history/src/bot_20250830093513.py | 141 -- .history/src/bot_20250830093531.py | 141 -- .history/src/bot_20250830093606.py | 142 -- .history/src/bot_20250830093645.py | 142 -- .history/src/bot_20250830093703.py | 142 -- .history/src/bot_20250830094738.py | 142 -- .history/src/bot_20250830100755.py | 142 -- .history/src/bot_20250830100926.py | 144 -- .history/src/bot_20250830101843.py | 144 -- .history/src/bot_20250830110611.py | 149 -- .history/src/bot_20250830110630.py | 154 -- .history/src/bot_20250830110906.py | 154 -- .history/src/bot_20250830141501.py | 155 -- .history/src/bot_20250830141515.py | 158 -- .history/src/bot_20250830141529.py | 165 -- .history/src/bot_20250830141957.py | 165 -- .history/src/config/config_20250830063519.py | 28 - .history/src/config/config_20250830063839.py | 28 - .history/src/config/config_20250830082127.py | 29 - .history/src/config/config_20250830082144.py | 29 - .history/src/config/config_20250830082223.py | 31 - .history/src/config/config_20250830082500.py | 31 - .history/src/config/config_20250830100958.py | 31 - .history/src/config/config_20250830101843.py | 31 - .../advanced_handlers_20250830091501.py | 864 -------- .../advanced_handlers_20250830092441.py | 864 -------- .../advanced_handlers_20250830093327.py | 912 -------- .../advanced_handlers_20250830093424.py | 972 -------- .../advanced_handlers_20250830093627.py | 972 -------- .../advanced_handlers_20250830094738.py | 972 -------- .../advanced_handlers_20250830104205.py | 972 -------- .../advanced_handlers_20250830104340.py | 972 -------- .../advanced_handlers_20250830105155.py | 982 --------- .../advanced_handlers_20250830105216.py | 980 -------- .../advanced_handlers_20250830110338.py | 980 -------- .../command_handlers_20250830063638.py | 275 --- .../command_handlers_20250830063839.py | 275 --- .../command_handlers_20250830065335.py | 282 --- .../command_handlers_20250830065348.py | 286 --- .../command_handlers_20250830065454.py | 286 --- .../command_handlers_20250830073032.py | 293 --- .../command_handlers_20250830073043.py | 293 --- .../command_handlers_20250830073339.py | 300 --- .../command_handlers_20250830073407.py | 311 --- .../command_handlers_20250830073425.py | 311 --- .../command_handlers_20250830073858.py | 328 --- .../command_handlers_20250830073916.py | 327 --- .../command_handlers_20250830074106.py | 380 ---- .../command_handlers_20250830074122.py | 383 ---- .../command_handlers_20250830074140.py | 383 ---- .../command_handlers_20250830083412.py | 385 ---- .../command_handlers_20250830083502.py | 385 ---- .../command_handlers_20250830092806.py | 331 --- .../command_handlers_20250830094738.py | 331 --- .../command_handlers_20250830110734.py | 328 --- .../command_handlers_20250830110754.py | 329 --- .../command_handlers_20250830110810.py | 325 --- .../command_handlers_20250830110839.py | 322 --- .../command_handlers_20250830110906.py | 322 --- .../extended_handlers_20250830065246.py | 255 --- .../extended_handlers_20250830065455.py | 255 --- .../extended_handlers_20250830073718.py | 259 --- .../extended_handlers_20250830073739.py | 263 --- .../extended_handlers_20250830073759.py | 273 --- .../extended_handlers_20250830073819.py | 277 --- .../extended_handlers_20250830073837.py | 281 --- .../extended_handlers_20250830074140.py | 281 --- .../extended_handlers_20250830083308.py | 345 --- .../extended_handlers_20250830083502.py | 345 --- .../extended_handlers_20250830095429.py | 349 --- .../extended_handlers_20250830095445.py | 352 --- .../extended_handlers_20250830095502.py | 355 --- .../extended_handlers_20250830095518.py | 358 --- .../extended_handlers_20250830095533.py | 361 --- .../extended_handlers_20250830095550.py | 364 --- .../extended_handlers_20250830095606.py | 367 --- .../extended_handlers_20250830095651.py | 367 --- .../extended_handlers_20250830104501.py | 378 ---- .../extended_handlers_20250830104715.py | 378 ---- .../handlers/help_handlers_20250830091943.py | 82 - .../handlers/help_handlers_20250830091955.py | 82 - .../handlers/help_handlers_20250830092004.py | 82 - .../handlers/help_handlers_20250830092014.py | 85 - .../handlers/help_handlers_20250830092029.py | 86 - .../handlers/help_handlers_20250830092040.py | 87 - .../handlers/help_handlers_20250830092051.py | 92 - .../handlers/help_handlers_20250830092139.py | 111 - .../handlers/help_handlers_20250830092441.py | 111 - .../handlers/help_handlers_20250830095731.py | 111 - .../handlers/help_handlers_20250830095750.py | 111 - .../handlers/help_handlers_20250830110705.py | 116 - .../handlers/help_handlers_20250830110906.py | 116 - .history/src/healthcheck_20250830102839.py | 71 - .history/src/healthcheck_20250830103154.py | 71 - .../src/utils/admin_utils_20250830110540.py | 283 --- .../src/utils/admin_utils_20250830110906.py | 283 --- .../src/utils/admin_utils_20250830114406.py | 302 --- .../src/utils/admin_utils_20250830114514.py | 283 --- .../src/utils/admin_utils_20250830142344.py | 295 --- .../src/utils/admin_utils_20250830142408.py | 298 --- .../src/utils/admin_utils_20250830142452.py | 301 --- .../src/utils/admin_utils_20250830142546.py | 303 --- .../src/utils/admin_utils_20250830142616.py | 310 --- .../src/utils/admin_utils_20250830142633.py | 312 --- .../src/utils/admin_utils_20250830142645.py | 314 --- .../src/utils/admin_utils_20250830142656.py | 317 --- .../src/utils/admin_utils_20250830143155.py | 317 --- .history/src/utils/logger_20250830063702.py | 45 - .history/src/utils/logger_20250830063839.py | 45 - .history/test_api_headers_20250830084440.py | 391 ---- .history/test_api_headers_20250830084500.py | 391 ---- .history/test_reboot_20250830083539.py | 73 - .history/test_reboot_20250830083624.py | 73 - .history/test_system_info_20250830083606.py | 87 - .history/test_system_info_20250830083624.py | 87 - .history/ОТЧЕТ_ПО_API_20250830090431.md | 185 -- .history/ОТЧЕТ_ПО_API_20250830090511.md | 185 -- 355 files changed, 1 insertion(+), 158423 deletions(-) delete mode 100644 .env.example delete mode 100644 .history/.dockerignore_20250830102713 delete mode 100644 .history/.dockerignore_20250830103154 delete mode 100644 .history/.dockerignore_20250830103255 delete mode 100644 .history/.env-example_20250830103005 delete mode 100644 .history/.env-example_20250830103154 delete mode 100644 .history/.env_20250830063713 delete mode 100644 .history/.env_20250830063839 delete mode 100644 .history/.env_20250830071119 delete mode 100644 .history/.env_20250830071139 delete mode 100644 .history/.env_20250830071153 delete mode 100644 .history/.env_20250830071249 delete mode 100644 .history/.env_20250830071300 delete mode 100644 .history/.env_20250830072439.example delete mode 100644 .history/.env_20250830072817.example delete mode 100644 .history/.env_20250830080745 delete mode 100644 .history/.env_20250830082200 delete mode 100644 .history/.env_20250830082210 delete mode 100644 .history/.env_20250830082500 delete mode 100644 .history/.env_20250830083237 delete mode 100644 .history/.env_20250830101005 delete mode 100644 .history/.env_20250830101843 delete mode 100644 .history/.gitignore_20250830063748 delete mode 100644 .history/.gitignore_20250830063839 delete mode 100644 .history/DOCKER_DEPLOYMENT_20250830103243.md delete mode 100644 .history/DOCKER_DEPLOYMENT_20250830103340.md delete mode 100644 .history/Dockerfile_20250830102631 delete mode 100644 .history/Dockerfile_20250830102802 delete mode 100644 .history/Dockerfile_20250830103154 delete mode 100644 .history/README_20250830063733.md delete mode 100644 .history/README_20250830063839.md delete mode 100644 .history/README_20250830065402.md delete mode 100644 .history/README_20250830065412.md delete mode 100644 .history/README_20250830065454.md delete mode 100644 .history/README_20250830072408.md delete mode 100644 .history/README_20250830072817.md delete mode 100644 .history/README_20250830092253.md delete mode 100644 .history/README_20250830092310.md delete mode 100644 .history/README_20250830092330.md delete mode 100644 .history/README_20250830092350.md delete mode 100644 .history/README_20250830092440.md delete mode 100644 .history/README_20250830103059.md delete mode 100644 .history/README_20250830103119.md delete mode 100644 .history/README_20250830103139.md delete mode 100644 .history/README_20250830103154.md delete mode 100644 .history/README_DOCKER_20250830102736.md delete mode 100644 .history/README_DOCKER_20250830103154.md delete mode 100644 .history/deploy_20250830102934.sh delete mode 100644 .history/deploy_20250830102949.cmd delete mode 100644 .history/deploy_20250830103154.cmd delete mode 100644 .history/deploy_20250830103154.sh delete mode 100644 .history/diagnose_api_20250830081925.py delete mode 100644 .history/diagnose_api_20250830081957.py delete mode 100644 .history/direct_api_test_20250830084231.py delete mode 100644 .history/direct_api_test_20250830084257.py delete mode 100644 .history/docker-compose_20250830102643.yml delete mode 100644 .history/docker-compose_20250830102820.yml delete mode 100644 .history/docker-compose_20250830102916.yml delete mode 100644 .history/docker-compose_20250830103154.yml delete mode 100644 .history/docs/file_manager_agent_20250830141933.md delete mode 100644 .history/docs/file_manager_agent_20250830141957.md delete mode 100644 .history/entrypoint_20250830102747.sh delete mode 100644 .history/entrypoint_20250830103154.sh delete mode 100644 .history/examples/file_manager_demo_20250830141907.py delete mode 100644 .history/examples/file_manager_demo_20250830141957.py delete mode 100644 .history/requirements_20250830063740.txt delete mode 100644 .history/requirements_20250830063839.txt delete mode 100644 .history/requirements_20250830065002.txt delete mode 100644 .history/requirements_20250830065455.txt delete mode 100644 .history/requirements_20250830072350.txt delete mode 100644 .history/requirements_20250830072817.txt delete mode 100644 .history/requirements_20250830092418.txt delete mode 100644 .history/requirements_20250830092441.txt delete mode 100644 .history/run_20250830063754.py delete mode 100644 .history/run_20250830063839.py delete mode 100644 .history/run_20250830102904.py delete mode 100644 .history/run_20250830103154.py delete mode 100644 .history/run_bot_20250830082521.py delete mode 100644 .history/run_bot_20250830082536.py delete mode 100644 .history/run_bot_20250830142127.py delete mode 100644 .history/run_bot_20250830142131.py delete mode 100644 .history/src/agents/__init___20250830141428.py delete mode 100644 .history/src/agents/__init___20250830141957.py delete mode 100644 .history/src/agents/file_manager_agent_20250830141230.py delete mode 100644 .history/src/agents/file_manager_agent_20250830141546.py delete mode 100644 .history/src/agents/file_manager_agent_20250830141613.py delete mode 100644 .history/src/agents/file_manager_agent_20250830141646.py delete mode 100644 .history/src/agents/file_manager_agent_20250830141721.py delete mode 100644 .history/src/agents/file_manager_agent_20250830141747.py delete mode 100644 .history/src/agents/file_manager_agent_20250830141805.py delete mode 100644 .history/src/agents/file_manager_agent_20250830141832.py delete mode 100644 .history/src/agents/file_manager_agent_20250830141847.py delete mode 100644 .history/src/agents/file_manager_agent_20250830141957.py delete mode 100644 .history/src/agents/file_manager_agent_20250830142055.py delete mode 100644 .history/src/agents/file_manager_agent_20250830142117.py delete mode 100644 .history/src/agents/file_manager_agent_20250830142754.py delete mode 100644 .history/src/agents/file_manager_agent_20250830142812.py delete mode 100644 .history/src/agents/file_manager_agent_20250830142848.py delete mode 100644 .history/src/agents/file_manager_agent_20250830142901.py delete mode 100644 .history/src/agents/file_manager_agent_20250830142941.py delete mode 100644 .history/src/agents/file_manager_agent_20250830143005.py delete mode 100644 .history/src/agents/file_manager_agent_20250830143049.py delete mode 100644 .history/src/agents/file_manager_agent_20250830143114.py delete mode 100644 .history/src/agents/file_manager_agent_20250830143155.py delete mode 100644 .history/src/agents/file_manager_agent_20250830143317.py delete mode 100644 .history/src/agents/file_manager_agent_20250830143333.py delete mode 100644 .history/src/agents/file_manager_agent_20250830143351.py delete mode 100644 .history/src/agents/file_manager_agent_20250830143422.py delete mode 100644 .history/src/agents/file_manager_agent_20250830143501.py delete mode 100644 .history/src/agents/file_manager_agent_20250830143531.py delete mode 100644 .history/src/agents/file_manager_agent_20250830143559.py delete mode 100644 .history/src/agents/file_manager_agent_20250830143628.py delete mode 100644 .history/src/agents/file_manager_agent_20250830143646.py delete mode 100644 .history/src/api/api_discovery_20250830081819.py delete mode 100644 .history/src/api/api_discovery_20250830081957.py delete mode 100644 .history/src/api/api_version_resolver_20250830084129.py delete mode 100644 .history/src/api/api_version_resolver_20250830084257.py delete mode 100644 .history/src/api/filestation_20250830141415.py delete mode 100644 .history/src/api/filestation_20250830141957.py delete mode 100644 .history/src/api/synology_20250830063552.py delete mode 100644 .history/src/api/synology_20250830063839.py delete mode 100644 .history/src/api/synology_20250830065021.py delete mode 100644 .history/src/api/synology_20250830065110.py delete mode 100644 .history/src/api/synology_20250830065154.py delete mode 100644 .history/src/api/synology_20250830065454.py delete mode 100644 .history/src/api/synology_20250830071505.py delete mode 100644 .history/src/api/synology_20250830071525.py delete mode 100644 .history/src/api/synology_20250830071727.py delete mode 100644 .history/src/api/synology_20250830071755.py delete mode 100644 .history/src/api/synology_20250830071934.py delete mode 100644 .history/src/api/synology_20250830072031.py delete mode 100644 .history/src/api/synology_20250830072122.py delete mode 100644 .history/src/api/synology_20250830072817.py delete mode 100644 .history/src/api/synology_20250830073007.py delete mode 100644 .history/src/api/synology_20250830073043.py delete mode 100644 .history/src/api/synology_20250830073131.py delete mode 100644 .history/src/api/synology_20250830073142.py delete mode 100644 .history/src/api/synology_20250830073153.py delete mode 100644 .history/src/api/synology_20250830073204.py delete mode 100644 .history/src/api/synology_20250830073217.py delete mode 100644 .history/src/api/synology_20250830073544.py delete mode 100644 .history/src/api/synology_20250830073606.py delete mode 100644 .history/src/api/synology_20250830073620.py delete mode 100644 .history/src/api/synology_20250830073939.py delete mode 100644 .history/src/api/synology_20250830073954.py delete mode 100644 .history/src/api/synology_20250830074025.py delete mode 100644 .history/src/api/synology_20250830074140.py delete mode 100644 .history/src/api/synology_20250830074228.py delete mode 100644 .history/src/api/synology_20250830074245.py delete mode 100644 .history/src/api/synology_20250830074313.py delete mode 100644 .history/src/api/synology_20250830074442.py delete mode 100644 .history/src/api/synology_20250830074627.py delete mode 100644 .history/src/api/synology_20250830074636.py delete mode 100644 .history/src/api/synology_20250830074708.py delete mode 100644 .history/src/api/synology_20250830074721.py delete mode 100644 .history/src/api/synology_20250830074730.py delete mode 100644 .history/src/api/synology_20250830074755.py delete mode 100644 .history/src/api/synology_20250830074838.py delete mode 100644 .history/src/api/synology_20250830074850.py delete mode 100644 .history/src/api/synology_20250830074920.py delete mode 100644 .history/src/api/synology_20250830074947.py delete mode 100644 .history/src/api/synology_20250830075007.py delete mode 100644 .history/src/api/synology_20250830075041.py delete mode 100644 .history/src/api/synology_20250830075053.py delete mode 100644 .history/src/api/synology_20250830075107.py delete mode 100644 .history/src/api/synology_20250830075248.py delete mode 100644 .history/src/api/synology_20250830075326.py delete mode 100644 .history/src/api/synology_20250830075348.py delete mode 100644 .history/src/api/synology_20250830075503.py delete mode 100644 .history/src/api/synology_20250830075522.py delete mode 100644 .history/src/api/synology_20250830080635.py delete mode 100644 .history/src/api/synology_20250830080658.py delete mode 100644 .history/src/api/synology_20250830080742.py delete mode 100644 .history/src/api/synology_20250830080825.py delete mode 100644 .history/src/api/synology_20250830080858.py delete mode 100644 .history/src/api/synology_20250830081426.py delete mode 100644 .history/src/api/synology_20250830081505.py delete mode 100644 .history/src/api/synology_20250830081538.py delete mode 100644 .history/src/api/synology_20250830081615.py delete mode 100644 .history/src/api/synology_20250830081654.py delete mode 100644 .history/src/api/synology_20250830081744.py delete mode 100644 .history/src/api/synology_20250830081837.py delete mode 100644 .history/src/api/synology_20250830081856.py delete mode 100644 .history/src/api/synology_20250830081957.py delete mode 100644 .history/src/api/synology_20250830082235.py delete mode 100644 .history/src/api/synology_20250830082307.py delete mode 100644 .history/src/api/synology_20250830082353.py delete mode 100644 .history/src/api/synology_20250830082444.py delete mode 100644 .history/src/api/synology_20250830082500.py delete mode 100644 .history/src/api/synology_20250830082853.py delete mode 100644 .history/src/api/synology_20250830082954.py delete mode 100644 .history/src/api/synology_20250830083115.py delete mode 100644 .history/src/api/synology_20250830084539.py delete mode 100644 .history/src/api/synology_20250830084644.py delete mode 100644 .history/src/api/synology_20250830084803.py delete mode 100644 .history/src/api/synology_20250830090902.py delete mode 100644 .history/src/api/synology_20250830090936.py delete mode 100644 .history/src/api/synology_20250830091024.py delete mode 100644 .history/src/api/synology_20250830091124.py delete mode 100644 .history/src/api/synology_20250830091218.py delete mode 100644 .history/src/api/synology_20250830092441.py delete mode 100644 .history/src/api/synology_20250830095113.py delete mode 100644 .history/src/api/synology_20250830095635.py delete mode 100644 .history/src/api/synology_20250830095651.py delete mode 100644 .history/src/api/synology_20250830100555.py delete mode 100644 .history/src/api/synology_20250830101023.py delete mode 100644 .history/src/api/synology_20250830101047.py delete mode 100644 .history/src/api/synology_20250830101104.py delete mode 100644 .history/src/api/synology_20250830101145.py delete mode 100644 .history/src/api/synology_20250830101211.py delete mode 100644 .history/src/api/synology_20250830101236.py delete mode 100644 .history/src/api/synology_20250830101843.py delete mode 100644 .history/src/api/synology_20250830104812.py delete mode 100644 .history/src/api/synology_20250830104833.py delete mode 100644 .history/src/api/synology_20250830104945.py delete mode 100644 .history/src/api/synology_20250830105105.py delete mode 100644 .history/src/api/synology_20250830105130.py delete mode 100644 .history/src/api/synology_20250830110338.py delete mode 100644 .history/src/bot_20250830063649.py delete mode 100644 .history/src/bot_20250830063839.py delete mode 100644 .history/src/bot_20250830065301.py delete mode 100644 .history/src/bot_20250830065311.py delete mode 100644 .history/src/bot_20250830065454.py delete mode 100644 .history/src/bot_20250830072835.py delete mode 100644 .history/src/bot_20250830072844.py delete mode 100644 .history/src/bot_20250830075657.py delete mode 100644 .history/src/bot_20250830075723.py delete mode 100644 .history/src/bot_20250830075740.py delete mode 100644 .history/src/bot_20250830075757.py delete mode 100644 .history/src/bot_20250830083325.py delete mode 100644 .history/src/bot_20250830083341.py delete mode 100644 .history/src/bot_20250830083502.py delete mode 100644 .history/src/bot_20250830091533.py delete mode 100644 .history/src/bot_20250830091644.py delete mode 100644 .history/src/bot_20250830092152.py delete mode 100644 .history/src/bot_20250830092440.py delete mode 100644 .history/src/bot_20250830093455.py delete mode 100644 .history/src/bot_20250830093513.py delete mode 100644 .history/src/bot_20250830093531.py delete mode 100644 .history/src/bot_20250830093606.py delete mode 100644 .history/src/bot_20250830093645.py delete mode 100644 .history/src/bot_20250830093703.py delete mode 100644 .history/src/bot_20250830094738.py delete mode 100644 .history/src/bot_20250830100755.py delete mode 100644 .history/src/bot_20250830100926.py delete mode 100644 .history/src/bot_20250830101843.py delete mode 100644 .history/src/bot_20250830110611.py delete mode 100644 .history/src/bot_20250830110630.py delete mode 100644 .history/src/bot_20250830110906.py delete mode 100644 .history/src/bot_20250830141501.py delete mode 100644 .history/src/bot_20250830141515.py delete mode 100644 .history/src/bot_20250830141529.py delete mode 100644 .history/src/bot_20250830141957.py delete mode 100644 .history/src/config/config_20250830063519.py delete mode 100644 .history/src/config/config_20250830063839.py delete mode 100644 .history/src/config/config_20250830082127.py delete mode 100644 .history/src/config/config_20250830082144.py delete mode 100644 .history/src/config/config_20250830082223.py delete mode 100644 .history/src/config/config_20250830082500.py delete mode 100644 .history/src/config/config_20250830100958.py delete mode 100644 .history/src/config/config_20250830101843.py delete mode 100644 .history/src/handlers/advanced_handlers_20250830091501.py delete mode 100644 .history/src/handlers/advanced_handlers_20250830092441.py delete mode 100644 .history/src/handlers/advanced_handlers_20250830093327.py delete mode 100644 .history/src/handlers/advanced_handlers_20250830093424.py delete mode 100644 .history/src/handlers/advanced_handlers_20250830093627.py delete mode 100644 .history/src/handlers/advanced_handlers_20250830094738.py delete mode 100644 .history/src/handlers/advanced_handlers_20250830104205.py delete mode 100644 .history/src/handlers/advanced_handlers_20250830104340.py delete mode 100644 .history/src/handlers/advanced_handlers_20250830105155.py delete mode 100644 .history/src/handlers/advanced_handlers_20250830105216.py delete mode 100644 .history/src/handlers/advanced_handlers_20250830110338.py delete mode 100644 .history/src/handlers/command_handlers_20250830063638.py delete mode 100644 .history/src/handlers/command_handlers_20250830063839.py delete mode 100644 .history/src/handlers/command_handlers_20250830065335.py delete mode 100644 .history/src/handlers/command_handlers_20250830065348.py delete mode 100644 .history/src/handlers/command_handlers_20250830065454.py delete mode 100644 .history/src/handlers/command_handlers_20250830073032.py delete mode 100644 .history/src/handlers/command_handlers_20250830073043.py delete mode 100644 .history/src/handlers/command_handlers_20250830073339.py delete mode 100644 .history/src/handlers/command_handlers_20250830073407.py delete mode 100644 .history/src/handlers/command_handlers_20250830073425.py delete mode 100644 .history/src/handlers/command_handlers_20250830073858.py delete mode 100644 .history/src/handlers/command_handlers_20250830073916.py delete mode 100644 .history/src/handlers/command_handlers_20250830074106.py delete mode 100644 .history/src/handlers/command_handlers_20250830074122.py delete mode 100644 .history/src/handlers/command_handlers_20250830074140.py delete mode 100644 .history/src/handlers/command_handlers_20250830083412.py delete mode 100644 .history/src/handlers/command_handlers_20250830083502.py delete mode 100644 .history/src/handlers/command_handlers_20250830092806.py delete mode 100644 .history/src/handlers/command_handlers_20250830094738.py delete mode 100644 .history/src/handlers/command_handlers_20250830110734.py delete mode 100644 .history/src/handlers/command_handlers_20250830110754.py delete mode 100644 .history/src/handlers/command_handlers_20250830110810.py delete mode 100644 .history/src/handlers/command_handlers_20250830110839.py delete mode 100644 .history/src/handlers/command_handlers_20250830110906.py delete mode 100644 .history/src/handlers/extended_handlers_20250830065246.py delete mode 100644 .history/src/handlers/extended_handlers_20250830065455.py delete mode 100644 .history/src/handlers/extended_handlers_20250830073718.py delete mode 100644 .history/src/handlers/extended_handlers_20250830073739.py delete mode 100644 .history/src/handlers/extended_handlers_20250830073759.py delete mode 100644 .history/src/handlers/extended_handlers_20250830073819.py delete mode 100644 .history/src/handlers/extended_handlers_20250830073837.py delete mode 100644 .history/src/handlers/extended_handlers_20250830074140.py delete mode 100644 .history/src/handlers/extended_handlers_20250830083308.py delete mode 100644 .history/src/handlers/extended_handlers_20250830083502.py delete mode 100644 .history/src/handlers/extended_handlers_20250830095429.py delete mode 100644 .history/src/handlers/extended_handlers_20250830095445.py delete mode 100644 .history/src/handlers/extended_handlers_20250830095502.py delete mode 100644 .history/src/handlers/extended_handlers_20250830095518.py delete mode 100644 .history/src/handlers/extended_handlers_20250830095533.py delete mode 100644 .history/src/handlers/extended_handlers_20250830095550.py delete mode 100644 .history/src/handlers/extended_handlers_20250830095606.py delete mode 100644 .history/src/handlers/extended_handlers_20250830095651.py delete mode 100644 .history/src/handlers/extended_handlers_20250830104501.py delete mode 100644 .history/src/handlers/extended_handlers_20250830104715.py delete mode 100644 .history/src/handlers/help_handlers_20250830091943.py delete mode 100644 .history/src/handlers/help_handlers_20250830091955.py delete mode 100644 .history/src/handlers/help_handlers_20250830092004.py delete mode 100644 .history/src/handlers/help_handlers_20250830092014.py delete mode 100644 .history/src/handlers/help_handlers_20250830092029.py delete mode 100644 .history/src/handlers/help_handlers_20250830092040.py delete mode 100644 .history/src/handlers/help_handlers_20250830092051.py delete mode 100644 .history/src/handlers/help_handlers_20250830092139.py delete mode 100644 .history/src/handlers/help_handlers_20250830092441.py delete mode 100644 .history/src/handlers/help_handlers_20250830095731.py delete mode 100644 .history/src/handlers/help_handlers_20250830095750.py delete mode 100644 .history/src/handlers/help_handlers_20250830110705.py delete mode 100644 .history/src/handlers/help_handlers_20250830110906.py delete mode 100644 .history/src/healthcheck_20250830102839.py delete mode 100644 .history/src/healthcheck_20250830103154.py delete mode 100644 .history/src/utils/admin_utils_20250830110540.py delete mode 100644 .history/src/utils/admin_utils_20250830110906.py delete mode 100644 .history/src/utils/admin_utils_20250830114406.py delete mode 100644 .history/src/utils/admin_utils_20250830114514.py delete mode 100644 .history/src/utils/admin_utils_20250830142344.py delete mode 100644 .history/src/utils/admin_utils_20250830142408.py delete mode 100644 .history/src/utils/admin_utils_20250830142452.py delete mode 100644 .history/src/utils/admin_utils_20250830142546.py delete mode 100644 .history/src/utils/admin_utils_20250830142616.py delete mode 100644 .history/src/utils/admin_utils_20250830142633.py delete mode 100644 .history/src/utils/admin_utils_20250830142645.py delete mode 100644 .history/src/utils/admin_utils_20250830142656.py delete mode 100644 .history/src/utils/admin_utils_20250830143155.py delete mode 100644 .history/src/utils/logger_20250830063702.py delete mode 100644 .history/src/utils/logger_20250830063839.py delete mode 100644 .history/test_api_headers_20250830084440.py delete mode 100644 .history/test_api_headers_20250830084500.py delete mode 100644 .history/test_reboot_20250830083539.py delete mode 100644 .history/test_reboot_20250830083624.py delete mode 100644 .history/test_system_info_20250830083606.py delete mode 100644 .history/test_system_info_20250830083624.py delete mode 100644 .history/ОТЧЕТ_ПО_API_20250830090431.md delete mode 100644 .history/ОТЧЕТ_ПО_API_20250830090511.md 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 (особенно хранилища и общих папок) - -Дополнительно рекомендуется реализовать функции автоматического мониторинга и уведомления о важных событиях, таких как высокая температура, заканчивающееся место на дисках или необходимость обновления системы.