#!/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)