"""MY Network Sanic Blueprint - маршруты для работы с распределенной сетью.""" import asyncio import json import logging from datetime import datetime from pathlib import Path from typing import Dict, List, Optional, Any from sanic import Blueprint, Request from sanic.response import json as json_response, file as file_response from sanic.exceptions import SanicException from app.core.logging import get_logger logger = get_logger(__name__) # Создать blueprint для MY Network API bp = Blueprint("my_network", url_prefix="/api/my") def get_node_service(): """Получить сервис ноды.""" try: from app.core.my_network.node_service import get_node_service return get_node_service() except Exception as e: logger.error(f"Error getting node service: {e}") return None @bp.get("/node/info") async def get_node_info(request: Request): """Получить информацию о текущей ноде.""" try: node_service = get_node_service() if not node_service: return json_response( {"error": "MY Network service not available"}, status=503 ) node_info = await node_service.get_node_info() return json_response({ "success": True, "data": node_info, "timestamp": datetime.utcnow().isoformat() }) except Exception as e: logger.error(f"Error getting node info: {e}") return json_response({"error": str(e)}, status=500) @bp.get("/node/peers") async def get_node_peers(request: Request): """Получить список подключенных пиров.""" try: node_service = get_node_service() if not node_service: return json_response( {"error": "MY Network service not available"}, status=503 ) peers_info = await node_service.get_peers_info() return json_response({ "success": True, "data": { "connected_peers": peers_info["connected_peers"], "peer_count": peers_info["peer_count"], "peers": peers_info["peers"] }, "timestamp": datetime.utcnow().isoformat() }) except Exception as e: logger.error(f"Error getting peers: {e}") return json_response({"error": str(e)}, status=500) @bp.post("/node/peers/connect") async def connect_to_peer(request: Request): """Подключиться к новому пиру.""" try: peer_data = request.json peer_address = peer_data.get("address") if not peer_address: return json_response({"error": "Peer address is required"}, status=400) node_service = get_node_service() if not node_service: return json_response( {"error": "MY Network service not available"}, status=503 ) success = await node_service.peer_manager.connect_to_peer(peer_address) if success: return json_response({ "success": True, "message": f"Successfully connected to peer: {peer_address}", "timestamp": datetime.utcnow().isoformat() }) else: return json_response({"error": "Failed to connect to peer"}, status=400) except Exception as e: logger.error(f"Error connecting to peer: {e}") return json_response({"error": str(e)}, status=500) @bp.delete("/node/peers/") async def disconnect_peer(request: Request, peer_id: str): """Отключиться от пира.""" try: node_service = get_node_service() if not node_service: return json_response( {"error": "MY Network service not available"}, status=503 ) success = await node_service.peer_manager.disconnect_peer(peer_id) if success: return json_response({ "success": True, "message": f"Successfully disconnected from peer: {peer_id}", "timestamp": datetime.utcnow().isoformat() }) else: return json_response( {"error": "Peer not found or already disconnected"}, status=404 ) except Exception as e: logger.error(f"Error disconnecting peer: {e}") return json_response({"error": str(e)}, status=500) @bp.get("/content/list") async def get_content_list(request: Request): """Получить список доступного контента.""" try: # Получить параметры запроса limit = min(int(request.args.get("limit", 100)), 1000) offset = max(int(request.args.get("offset", 0)), 0) # Кэшировать результат на 5 минут from app.core.cache import cache cache_key = f"my_network:content_list:{limit}:{offset}" cached_result = await cache.get(cache_key) if cached_result: return json_response(json.loads(cached_result)) # Получить контент из БД from app.core.database import db_manager from app.core.models.content_compatible import Content, ContentMetadata from sqlalchemy import select, func async with db_manager.get_session() as session: stmt = ( select(Content, ContentMetadata) .outerjoin(ContentMetadata, Content.id == ContentMetadata.content_id) .where(Content.is_active == True) .order_by(Content.created_at.desc()) .limit(limit) .offset(offset) ) result = await session.execute(stmt) content_items = [] for content, metadata in result: content_data = { "hash": content.sha256_hash or content.md5_hash, "filename": content.filename, "original_filename": content.original_filename, "file_size": content.file_size, "file_type": content.file_type, "mime_type": content.mime_type, "created_at": content.created_at.isoformat(), "encrypted": getattr(content, 'encrypted', False), "metadata": metadata.to_dict() if metadata else {} } content_items.append(content_data) # Получить общее количество count_stmt = select(func.count(Content.id)).where(Content.is_active == True) count_result = await session.execute(count_stmt) total_count = count_result.scalar() response_data = { "success": True, "data": { "content": content_items, "total": total_count, "limit": limit, "offset": offset }, "timestamp": datetime.utcnow().isoformat() } # Кэшировать результат await cache.set(cache_key, json.dumps(response_data), expire=300) return json_response(response_data) except Exception as e: logger.error(f"Error getting content list: {e}") return json_response({"error": str(e)}, status=500) @bp.get("/content//exists") async def check_content_exists(request: Request, content_hash: str): """Проверить существование контента по хешу.""" try: # Кэшировать результат на 30 минут from app.core.cache import cache cache_key = f"my_network:content_exists:{content_hash}" cached_result = await cache.get(cache_key) if cached_result is not None: return json_response({"exists": cached_result == "true", "hash": content_hash}) # Проверить в БД from app.core.database import db_manager from app.core.models.content_compatible import Content from sqlalchemy import select, and_ async with db_manager.get_session() as session: stmt = select(Content.id).where( and_( Content.is_active == True, (Content.md5_hash == content_hash) | (Content.sha256_hash == content_hash) ) ) result = await session.execute(stmt) exists = result.scalar_one_or_none() is not None # Кэшировать результат await cache.set(cache_key, "true" if exists else "false", expire=1800) return json_response({ "exists": exists, "hash": content_hash, "timestamp": datetime.utcnow().isoformat() }) except Exception as e: logger.error(f"Error checking content existence: {e}") return json_response({"error": str(e)}, status=500) @bp.get("/sync/status") async def get_sync_status(request: Request): """Получить статус синхронизации.""" try: node_service = get_node_service() if not node_service: return json_response( {"error": "MY Network service not available"}, status=503 ) sync_status = await node_service.sync_manager.get_sync_status() return json_response({ "success": True, "data": sync_status, "timestamp": datetime.utcnow().isoformat() }) except Exception as e: logger.error(f"Error getting sync status: {e}") return json_response({"error": str(e)}, status=500) @bp.post("/sync/start") async def start_network_sync(request: Request): """Запустить синхронизацию с сетью.""" try: node_service = get_node_service() if not node_service: return json_response( {"error": "MY Network service not available"}, status=503 ) sync_result = await node_service.sync_manager.sync_with_network() return json_response({ "success": True, "data": sync_result, "timestamp": datetime.utcnow().isoformat() }) except Exception as e: logger.error(f"Error starting network sync: {e}") return json_response({"error": str(e)}, status=500) @bp.get("/network/stats") async def get_network_stats(request: Request): """Получить статистику сети.""" try: node_service = get_node_service() if not node_service: return json_response( {"error": "MY Network service not available"}, status=503 ) # Получить информацию о ноде и пирах node_info = await node_service.get_node_info() peers_info = await node_service.get_peers_info() sync_status = await node_service.sync_manager.get_sync_status() # Статистика контента from app.core.database import db_manager from app.core.models.content_compatible import Content from sqlalchemy import select, func async with db_manager.get_session() as session: # Общее количество контента content_count_stmt = select(func.count(Content.id)).where(Content.is_active == True) content_count_result = await session.execute(content_count_stmt) total_content = content_count_result.scalar() # Размер контента size_stmt = select(func.sum(Content.file_size)).where(Content.is_active == True) size_result = await session.execute(size_stmt) total_size = size_result.scalar() or 0 # Контент по типам type_stmt = select(Content.file_type, func.count(Content.id)).where(Content.is_active == True).group_by(Content.file_type) type_result = await session.execute(type_stmt) content_by_type = {row[0]: row[1] for row in type_result} network_stats = { "node_info": { "node_id": node_info["node_id"], "uptime": node_info["uptime"], "version": node_info["version"], "status": node_info["status"] }, "network": { "connected_peers": peers_info["peer_count"], "known_peers": len(peers_info["peers"]), "network_health": "good" if peers_info["peer_count"] > 0 else "isolated" }, "content": { "total_items": total_content, "total_size_bytes": total_size, "total_size_mb": round(total_size / (1024 * 1024), 2), "content_by_type": content_by_type }, "sync": { "active_syncs": sync_status["active_syncs"], "queue_size": sync_status["queue_size"], "is_running": sync_status["is_running"] } } return json_response({ "success": True, "data": network_stats, "timestamp": datetime.utcnow().isoformat() }) except Exception as e: logger.error(f"Error getting network stats: {e}") return json_response({"error": str(e)}, status=500) @bp.get("/health") async def health_check(request: Request): """Проверка здоровья MY Network ноды.""" try: node_service = get_node_service() # Базовая проверка сервисов health_status = { "status": "healthy", "timestamp": datetime.utcnow().isoformat(), "services": { "node_service": node_service is not None, "peer_manager": hasattr(node_service, 'peer_manager') if node_service else False, "sync_manager": hasattr(node_service, 'sync_manager') if node_service else False, "database": True # Если дошли до этой точки, БД работает } } # Проверить подключение к пирам if node_service: peers_info = await node_service.get_peers_info() health_status["network"] = { "connected_peers": peers_info["peer_count"], "status": "connected" if peers_info["peer_count"] > 0 else "isolated" } # Определить общий статус if not all(health_status["services"].values()): health_status["status"] = "unhealthy" elif node_service and peers_info["peer_count"] == 0: health_status["status"] = "isolated" return json_response(health_status) except Exception as e: logger.error(f"Health check failed: {e}") return json_response({ "status": "unhealthy", "error": str(e), "timestamp": datetime.utcnow().isoformat() }, status=500)