426 lines
15 KiB
Python
426 lines
15 KiB
Python
"""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/<peer_id>")
|
||
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/<content_hash>/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) |