file manager

This commit is contained in:
2025-08-30 14:42:08 +09:00
parent 3d189c415f
commit 5c263e6e5d
60 changed files with 29336 additions and 35 deletions

View File

@@ -0,0 +1,512 @@
#!/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)