Merge remote-tracking branch 'origin/routing_fix'

This commit is contained in:
user 2025-08-16 06:34:13 +03:00
commit cf1b715ab8
6 changed files with 384 additions and 17 deletions

View File

@ -2,16 +2,19 @@ FROM python:3.11-slim
WORKDIR /app
# Установка системных зависимостей
RUN apt-get update && apt-get install -y \
# Установка системных зависимостей (только необходимые)
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gcc \
g++ \
curl \
ffmpeg \
libmagic1 \
&& rm -rf /var/lib/apt/lists/*
# Копирование requirements и установка Python зависимостей
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копирование requirements и установка Python зависимостей
COPY requirements.txt .
RUN python -m pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
# Копирование кода приложения
COPY app/ ./app/

View File

@ -0,0 +1,326 @@
"""
Compatibility routes to preserve deprecated uploader-bot API surface (v1/system).
These endpoints mirror legacy paths so older clients continue to function,
while new v3 sync API works in parallel.
"""
import base64
import os
from typing import Optional, List
import aiofiles
from fastapi import APIRouter, UploadFile, File, HTTPException, Query
from fastapi.responses import JSONResponse, StreamingResponse, PlainTextResponse
from sqlalchemy import select
from app.core.logging import get_logger
from app.core.config import get_settings
from app.core.database import db_manager
from app.core.models.content_models import StoredContent as Content
from app.core.storage import LocalStorageBackend
router = APIRouter(prefix="", tags=["compat-v1"])
logger = get_logger(__name__)
settings = get_settings()
@router.get("/")
async def index_root():
return PlainTextResponse("MY Network Node", status_code=200)
@router.get("/favicon.ico")
async def favicon():
return PlainTextResponse("", status_code=204)
@router.get("/api/system.version")
async def system_version():
codebase_hash = os.getenv("CODEBASE_HASH", "unknown")
codebase_branch = os.getenv("CODEBASE_BRANCH", os.getenv("GIT_BRANCH", "main"))
return {"codebase_hash": codebase_hash, "codebase_branch": codebase_branch}
@router.post("/api/system.sendStatus")
async def system_send_status(payload: dict):
try:
message_b58 = payload.get("message")
signature = payload.get("signature")
if not message_b58 or not signature:
raise HTTPException(status_code=400, detail="message and signature required")
await logger.ainfo("Compat system.sendStatus", signature=signature)
return {"ok": True}
except HTTPException:
raise
except Exception as e:
await logger.aerror("sendStatus failed", error=str(e))
raise HTTPException(status_code=500, detail="sendStatus failed")
@router.get("/api/tonconnect-manifest.json")
async def tonconnect_manifest():
host = str(getattr(settings, "PROJECT_HOST", "")) or os.getenv("PROJECT_HOST", "") or "http://localhost:8000"
return {
"url": host,
"name": "MY Network Node",
"iconUrl": f"{host}/static/icon.png",
"termsOfUseUrl": f"{host}/terms",
"privacyPolicyUrl": f"{host}/privacy",
"bridgeUrl": "https://bridge.tonapi.io/bridge",
"manifestVersion": 2
}
@router.get("/api/platform-metadata.json")
async def platform_metadata():
host = str(getattr(settings, "PROJECT_HOST", "")) or os.getenv("PROJECT_HOST", "") or "http://localhost:8000"
return {
"name": "MY Network Platform",
"symbol": "MYN",
"description": "Decentralized content platform (v3)",
"image": f"{host}/static/platform.png",
"external_url": host,
"version": "3.0.0"
}
@router.get("/api/v1/node")
async def v1_node():
from app.core.crypto import get_ed25519_manager
cm = get_ed25519_manager()
return {
"id": cm.node_id,
"node_address": "",
"master_address": "",
"indexer_height": 0,
"services": {}
}
@router.get("/api/v1/nodeFriendly")
async def v1_node_friendly():
from app.core.crypto import get_ed25519_manager
cm = get_ed25519_manager()
return PlainTextResponse(f"Node ID: {cm.node_id}\nIndexer height: 0\nServices: none\n")
@router.post("/api/v1/auth.twa")
async def v1_auth_twa(payload: dict):
user_ref = payload.get("user") or {}
token = base64.b64encode(f"twa:{user_ref}".encode()).decode()
return {"token": token}
@router.get("/api/v1/auth.me")
async def v1_auth_me():
return {"user": None, "status": "guest"}
@router.post("/api/v1/auth.selectWallet")
async def v1_auth_select_wallet(payload: dict):
return {"ok": True}
@router.get("/api/v1/tonconnect.new")
async def v1_tonconnect_new():
return {"ok": True}
@router.post("/api/v1/tonconnect.logout")
async def v1_tonconnect_logout(payload: dict):
return {"ok": True}
@router.post("/api/v1.5/storage")
async def v1_5_storage_upload(file: UploadFile = File(...)):
return await v1_storage_upload(file)
@router.get("/api/v1.5/storage/{file_hash}")
async def v1_5_storage_get(file_hash: str):
return await v1_storage_get(file_hash)
@router.post("/api/v1/storage")
async def v1_storage_upload(file: UploadFile = File(...)):
try:
data = await file.read()
if not data:
raise HTTPException(status_code=400, detail="empty file")
backend = LocalStorageBackend()
file_hash = sha256(data).hexdigest()
file_path = os.path.join(backend.files_path, file_hash)
async with aiofiles.open(file_path, 'wb') as f:
await f.write(data)
async with db_manager.get_session() as session:
existing = await session.execute(select(Content).where(Content.hash == file_hash))
if existing.scalars().first() is None:
content = Content(
hash=file_hash,
filename=file.filename or file_hash,
file_size=len(data),
mime_type=file.content_type or "application/octet-stream",
file_path=str(file_path),
)
session.add(content)
await session.commit()
return {"hash": file_hash}
except HTTPException:
raise
except Exception as e:
await logger.aerror("v1 upload failed", error=str(e))
raise HTTPException(status_code=500, detail="upload failed")
@router.get("/api/v1/storage/{file_hash}")
async def v1_storage_get(file_hash: str):
try:
async with db_manager.get_session() as session:
result = await session.execute(select(Content).where(Content.hash == file_hash))
content = result.scalars().first()
if not content or not content.file_path:
raise HTTPException(status_code=404, detail="not found")
backend = LocalStorageBackend()
return StreamingResponse(backend.get_file_stream(content.file_path))
except HTTPException:
raise
except Exception as e:
await logger.aerror("v1 storage get failed", error=str(e))
raise HTTPException(status_code=500, detail="failed")
@router.get("/api/v1/storage.decodeContentId/{content_id}")
async def v1_decode_content_id(content_id: str):
try:
async with db_manager.get_session() as session:
result = await session.execute(select(Content).where(Content.id == content_id))
content = result.scalars().first()
if not content:
raise HTTPException(status_code=404, detail="not found")
return {
"id": content.id,
"hash": content.hash,
"filename": content.filename,
"size": content.file_size,
"mime_type": content.mime_type,
}
except HTTPException:
raise
except Exception as e:
await logger.aerror("decodeContentId failed", error=str(e))
raise HTTPException(status_code=500, detail="failed")
@router.get("/api/v1/content.list")
async def v1_content_list(limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0)):
try:
async with db_manager.get_session() as session:
result = await session.execute(select(Content).offset(offset).limit(limit))
items: List[Content] = result.scalars().all()
return {
"items": [
{
"id": it.id,
"hash": it.hash,
"filename": it.filename,
"size": it.file_size,
"mime_type": it.mime_type,
} for it in items
],
"limit": limit,
"offset": offset
}
except Exception as e:
await logger.aerror("content.list failed", error=str(e))
raise HTTPException(status_code=500, detail="failed")
@router.get("/api/v1/content.view")
async def v1_content_view(hash: Optional[str] = None, id: Optional[str] = None):
try:
if not hash and not id:
raise HTTPException(status_code=400, detail="hash or id required")
async with db_manager.get_session() as session:
stmt = select(Content)
if hash:
stmt = stmt.where(Content.hash == hash)
if id:
stmt = stmt.where(Content.id == id)
result = await session.execute(stmt)
content = result.scalars().first()
if not content:
raise HTTPException(status_code=404, detail="not found")
return {
"id": content.id,
"hash": content.hash,
"filename": content.filename,
"size": content.file_size,
"mime_type": content.mime_type,
"created_at": getattr(content, "created_at", None)
}
except HTTPException:
raise
except Exception as e:
await logger.aerror("content.view failed", error=str(e))
raise HTTPException(status_code=500, detail="failed")
@router.get("/api/v1/content.view/{content_address}")
async def v1_content_view_path(content_address: str):
try:
async with db_manager.get_session() as session:
result = await session.execute(select(Content).where((Content.id == content_address) | (Content.hash == content_address)))
content = result.scalars().first()
if not content:
raise HTTPException(status_code=404, detail="not found")
return {
"id": content.id,
"hash": content.hash,
"filename": content.filename,
"size": content.file_size,
"mime_type": content.mime_type,
"created_at": getattr(content, "created_at", None)
}
except HTTPException:
raise
except Exception as e:
await logger.aerror("content.view(path) failed", error=str(e))
raise HTTPException(status_code=500, detail="failed")
@router.get("/api/v1/content.friendlyList")
async def v1_content_friendly_list(limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0)):
return await v1_content_list(limit, offset)
@router.get("/api/v1.5/content.list")
async def v1_5_content_list(limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0)):
return await v1_content_list(limit, offset)
@router.post("/api/v1/blockchain.sendNewContentMessage")
async def v1_chain_send_new_content(payload: dict):
await logger.ainfo("compat blockchain.sendNewContentMessage", payload=payload)
return {"ok": True}
@router.post("/api/v1/blockchain.sendPurchaseContent")
async def v1_chain_send_purchase(payload: dict):
await logger.ainfo("compat blockchain.sendPurchaseContent", payload=payload)
return {"ok": True}
@router.post("/api/v1/blockchain.sendPurchaseContentMessage")
async def v1_chain_send_purchase_message(payload: dict):
await logger.ainfo("compat blockchain.sendPurchaseContentMessage", payload=payload)
return {"ok": True}
@router.get("/api/v1/account")
async def v1_account():
return {"ok": True}

View File

@ -10,6 +10,7 @@ from fastapi.responses import JSONResponse
from app.core.crypto import get_ed25519_manager
from app.core.logging import get_logger
from app.core.database import get_cache_manager
logger = get_logger(__name__)
@ -44,6 +45,24 @@ async def validate_node_request(request: Request) -> Dict[str, Any]:
try:
message_data = json.loads(body.decode())
# Anti-replay: validate timestamp and nonce
try:
ts = message_data.get("timestamp")
nonce = message_data.get("nonce")
if ts:
from datetime import datetime, timezone
now = datetime.now(timezone.utc).timestamp()
if abs(float(ts) - float(now)) > 300:
raise HTTPException(status_code=400, detail="stale timestamp")
if nonce:
cache = await get_cache_manager()
cache_key = f"replay:{node_id}:{nonce}"
if await cache.get(cache_key):
raise HTTPException(status_code=400, detail="replay detected")
await cache.set(cache_key, True, ttl=600)
except Exception:
# Backward compatible: missing fields
pass
except json.JSONDecodeError:
raise HTTPException(status_code=400, detail="Invalid JSON in request body")

View File

@ -39,6 +39,8 @@ from app.api.fastapi_content_routes import router as content_router
from app.api.fastapi_storage_routes import router as storage_router
from app.api.fastapi_node_routes import router as node_router
from app.api.fastapi_system_routes import router as system_router
from app.api.fastapi_compat_routes import router as compat_router
from app.api.fastapi_v3_routes import router as v3_router
# ДОБАВЛЕНО: импорт дополнительных роутеров из app/api/routes/
from app.api.routes.content_access_routes import router as content_access_router
@ -226,6 +228,8 @@ def create_fastapi_app() -> FastAPI:
# ВНИМАНИЕ: эти роутеры уже имеют собственные префиксы (prefix=...), поэтому include без доп. prefix
app.include_router(content_access_router) # /api/content/*
app.include_router(node_stats_router) # /api/node/stats/*
app.include_router(compat_router) # legacy v1/system
app.include_router(v3_router) # /api/v3/*
# Дополнительные обработчики событий
setup_exception_handlers(app)
@ -310,6 +314,8 @@ def setup_exception_handlers(app: FastAPI):
# Увеличиваем счетчик ошибок для мониторинга
from app.api.fastapi_system_routes import increment_error_counter
from app.api.fastapi_compat_routes import router as compat_router
from app.api.fastapi_v3_routes import router as v3_router
await increment_error_counter()
return JSONResponse(
@ -334,6 +340,8 @@ def setup_middleware_hooks(app: FastAPI):
# Увеличиваем счетчик запросов
from app.api.fastapi_system_routes import increment_request_counter
from app.api.fastapi_compat_routes import router as compat_router
from app.api.fastapi_v3_routes import router as v3_router
await increment_request_counter()
# Проверяем режим обслуживания
@ -379,6 +387,8 @@ def setup_middleware_hooks(app: FastAPI):
)
from app.api.fastapi_system_routes import increment_error_counter
from app.api.fastapi_compat_routes import router as compat_router
from app.api.fastapi_v3_routes import router as v3_router
await increment_error_counter()
raise

View File

@ -1,14 +1,14 @@
{
"version": "3.0.0",
"network_id": "my-network-1754810662",
"created_at": "2025-08-10T07:24:22Z",
"network_id": "my-network-1755263533",
"created_at": "2025-08-15T13:12:13Z",
"bootstrap_nodes": [
{
"id": "node-7fd144167286645c",
"node_id": "node-7fd144167286645c",
"id": "node-e3ebfd8e2444dd4f",
"node_id": "node-e3ebfd8e2444dd4f",
"address": "2a02:6b40:2000:16b1::1",
"port": 8000,
"public_key": "7fd144167286645c3b01cd16d87a775f57ec134dbad412e13f3016c33e936177",
"public_key": "e3ebfd8e2444dd4f8747472a3c753708e45a47b16f33401790caa5c5ca67534d",
"trusted": true,
"node_type": "bootstrap"
}

View File

@ -8,6 +8,9 @@ asyncpg==0.29.0
redis==5.0.1
aioredis==2.0.1
aiofiles==23.2.1
aiohttp==3.12.15
yarl==1.17.1
multidict==6.0.5
cryptography==41.0.7
python-jose[cryptography]==3.3.0
python-multipart==0.0.6
@ -28,10 +31,16 @@ python-magic==0.4.27
jinja2==3.1.2
starlette==0.27.0
structlog==23.2.0
aiogram==3.3.0
aiogram==3.21.0
magic-filter==1.0.12
sanic==23.12.1
PyJWT==2.8.0
cryptography==41.0.7
ed25519==1.5
tonsdk==1.0.15
pytonconnect==0.3.0
pytonconnect==0.3.2
prometheus-client==0.22.1
pydub==0.25.1
pycryptodome==3.23.0
psycopg2-binary==2.9.10
PyNaCl==1.5.0
uvloop==0.21.0